Server-Side Javascript in Freemarker Templates

Titania Delivery provides a number of directives for evaluating Javascript on the server as part of rendering.

Freemarker is robust enough to almost function as a fully-fledged programming language. However, it does have some limitations in this regard, and there are times when it would be useful for certain logic-intensive or computational processing.

The following directives will invoke Javascript while processing a template. These directives will be available to both online and packager templates.

<@js>

This directive will allow users to execute javascript at the current location in the template. The javascript code can use page.var(name) to get the value of a Freemarker variable from the page, and use page.assign(name, value) and/or page.local(name, value) to mimic the behavior of the Freemarker <#assign> and <#local> directives. Calling the print() function will write the given value to the current location in the template. In addition, the td.load() function can be used to source an external script from somewhere in the theme.

Functions and variables declared in one <@js> directive will be available to all <@js> directives and <@jsFunction> definitions declared later in the package. In Javascript terms, they share the same global scope.

For example, the following would calculate the base name of a document's filename, assign it to the Freemarker variable 'baseName', and write it to the output.

<@js>
  var doc = page.var('doc');
  var name = doc.label;
  var ndx = name.lastIndexOf('.');
  if (ndx !== -1) {
    name = name.substring(0, index);
  }
  page.assign('baseName', name);
  print(name);
</@js>

The js and jsFunction directives are implemented using the Nashorn script engine, which includes some javascript extensions for integration with Java. See the Nashorn Java Scripting Programmer's Guide. Our implementation does not permit instantiation of arbitrary java classes.

One particular problem for script authors will be handling Freemarker sequences in javascript. The directive implementation handles conversion of top-level sequence variables into javascript as true javascript arrays. It also will convert javascript arrays to Freemarker sequences when assigned using page.assign() or page.local() functions. However, Freemarker sequences in subvariables (hash values) appear in javascript as generic list objects. As such, they do not provide all the usual javascript array functions, such as sort(), join(), etc. In order to call these functions on a sequence obtained from a hash value, use the Nashorn extension function, Java.from() to cast the sequence to a javascript array. The following example shows the different ways Freemarker sequences can be used in javascript.

<@js>
  // 'docs' will be a true javascript array, automatically converted from 
  // a Freemarker sequence by the page.var() function.
  var docs = page.var('documents'); 
  
  // Sort the docs by title. doc.metadata.title is a sequence (List in javascript),
  // which can use the subscript operator, like an array. No need to cast to array.
  docs.sort(function(a,b) {
    return a.metadata.title[0] < b.metadata.title[0] ?
        -1 : a.metadata.title[0] > b.metadata.title[0] ?
        1 : 0;
      });
  for (var i in docs) {
    var doc = docs[i];
    // doc.keywords is a sequence that comes into javascript as a List.
    // We can get the length of the sequence.
    if (doc.keywords.length > 0) {
      // But must cast to javascript array to get full array behavior.
      // WARNING: doc.keywords.join(' ') will cause script exception.
      var kwords = Java.from(doc.keywords).join(' ');
      // Now 'kwords' is a string containing space-separated values of doc.keywords
    }
  }
</@js>
<@jsFunction>

This is similar to the built-in Freemarker directive <#function>, except that the body of the function is implemented in Javascript instead of Freemarker. It has two attributes, @name (the name of the function) and, optionally, @parameters, the comma-separated list of parameter names.

Here is a function that computes the base name of a document passed in as a parameter.

<@jsFunction name="getBaseName" parameters="doc">
  var name = doc.label;
  var ndx = name.lastIndexOf('.');
  return ndx === -1 ? name : name.substring(0, ndx);
</@jsFunction>
<#-- Called using normal FreeMarker function call semantics. -->
${getBaseName(doc)?html}

For additional details, see the discussion of script engine implementation under <@js>, above.