Word templates

Template syntax

A Moliri template is an ordinary Word file (.docx) with commands of the form {# … #} in its text.

Moliri reads the literal text of the document (body text, table cells, headers and footers) and runs each command as a JavaScript expression. Whether the command sits in a content control, as the template editor creates it, or as plain text makes no difference to this.

The commands

CommandFormEffect
INS{# expression #} or {# INS expression #}Insert the result as text. The bare expression is the short form
EXEC{#! code #} or {# EXEC code #}Run code, insert nothing. Variables assigned here are visible in all following commands
FOR / END-FOR{# FOR x IN list #} … {# END-FOR x #}Repeat the content for each list element
IF / END-IF{# IF condition #} … {# END-IF #}Content only when the condition is true
IMAGE{# IMAGE img(file, { width: 6 }) #}Insert an image
LINK{# LINK ({ url: `…`, label: `…` }) #}Insert a clickable link; without label the address is shown
HTML{# HTML `<b>bold</b>` #}Embed an HTML fragment; Word converts it when the file is opened
ALIAS{# ALIAS name command text #}, called as {#*name#}Shorthand for a complete command

Inside a command, JavaScript syntax applies. Strings in backticks and template literals (`${…}`) work.

{#$o[`RegNr`] ?? ``#}
{#! FLOAT_FORMAT = { "mask": "Number", "scale": "2", "thousandsSeparator": " ", "radix": "," }#}
{# IMAGE img(eingang[$i], { width: 16 }) #}
{# LINK ({ url: `https://beispiel.moliri.app/p/${p.project_id}/o/${$o.object_id}`, label: `Objekt öffnen` }) #}

The rule for loop variables

The most common source of errors. Inside a FOR loop, the loop variable is written with a $ in front:

{# FOR file IN $f #}
  {# IMAGE img($file, { width: 6 }) #}     ← right: $file
  {# IMAGE img(file, { width: 6 }) #}      ← wrong: file is undefined
{# END-FOR file #}

This applies to every loop: FOR obj IN objects gives $obj, FOR i IN _.range(0, n) gives $i. Every loop also provides $idx, the index of the innermost loop, starting at 0. A running number is therefore {#$idx + 1#}.

In a table, FOR and END-FOR sit in rows of their own, FOR above the data row and END-FOR below it. The data row is then repeated for each element. This is how object lists become tables, see Recipes . If both sit in the first and last cell of the same row, the built-in engine repeats only the cells, not the row.

Backticks instead of quotation marks

Word likes to replace straight quotation marks with typographic ones. Moliri evens this out on export, and the field detection understands typographic quotation marks too. Backticks are still the better choice. Word never changes them, and they allow template literals at the same time.

Simple output

{#$o[`RegNr`] ?? ``#}

$o is the current object, and the field name goes in square brackets. The empty fallback after ?? makes sure that a field without a value stays empty and causes no follow-on errors.

Condition

{# IF $o[`Inventarstatus`] === `Dauerleihgabe` #}
Dauerleihgabe
{# END-IF #}

Loop over objects

{# FOR obj IN objects #}
{# $obj[`RegNr`] ?? `` #}
{# $obj[`Originaltitel`] ?? `` #}

{# END-FOR obj #}

Field detection and loading

For the export, Moliri loads only the fields the template uses. It detects every field name that stands in square brackets and quotation marks: $o[`Feld`], $obj[`Feld`] in loops and also o[`Feld`] in a grouping function. For keys without $o or a loop variable in front, only names that match a field of the project count.

A field name that gets into the command another way is not detected, for example as a computed key or as a string in _.groupBy(objects, `Leihgeber`). You can make such a field visible in a condition that never runs:

{# IF false #}
{# INS $o[`Leihgeber`] ?? `` #}
{# END-IF #}

Moliri loads files only if the template mentions $f or files. This keeps pure data templates fast.

One document, or one per object

Moliri recognises the mode from the commands:

  • Loop over the objects, one document. As soon as a FOR loop runs over the set of objects, the template is rendered exactly once with all objects. Any FOR whose source names objects or a variable assigned from it is recognised: FOR obj IN objects, FOR obj IN objects.filter(…) and also {#! gruppen = _.groupBy(objects, …) #} with FOR key IN Object.keys(gruppen). File and index loops (FOR file IN $f, FOR i IN _.range(…)) do not count.
  • Otherwise one document per object, then merged or packed as a ZIP.

objects without a loop does not change the mode. {#objects.length#} in a per-object template outputs the total count and leaves it a per-object template.

Override the detection

If the detection gets it wrong, you can set the mode in the template editor: right-hand panel Moliri · Check, Settings tab, Document split selection with Automatic, One document with all objects and One document per object. The export dialog then shows “set manually”.

Section behaviour when merging

With one document per object, the section behaviour decides how the individual documents are joined:

ValueMeaning
templateThe template’s section behaviour (default). Headers and footers, margins and breaks follow the template
noneMerge without an extra break
pagebreakInsert only a plain page break
section-continuousSection break, continuous on the same page
section-newpageSection break, new page
section-evenpageSection break, next even page
section-oddpageSection break, next odd page
zipDo not merge, pack as a ZIP. One file per object

The values are stable and will not be renamed. The output file is named <template name>_<YYYY-MM-DD>.docx or .zip.

Settings travel in the document

In the right-hand panel (Settings tab), the template editor saves three settings as an invisible marker in the template: As plain text (resolve content controls), Document split (object mode) and Transition between objects (section behaviour). On export, Moliri reads the marker. The template therefore brings its own presets with it, whoever uses it.

If the marker is missing, for example in a template written in Word, the following applies: content controls are resolved to plain text, the mode is detected, and the section behaviour is template. In the export dialog you can override merging and content controls for one run under Advanced settings.

Execution and errors

TopicBehaviour
ExecutionCommands run as synchronous expressions, without await and async. img() works asynchronously internally; Moliri waits for it itself, and the template contains only the simple call
Arrow functionsWork and are common: $f.filter(f => …), sort((a, b) => …)
IsolationOn export and during the check, commands run in an isolated background process. Available are the helpers and the data that Moliri loads beforehand. No network (fetch, WebSocket, loading outside scripts), no browser storage (login, localStorage, IndexedDB). The check reports such a call as the error Not allowed: …
Run timeAt most 60 seconds per run, that is per document or per check. The time for loading images does not count. After that, Moliri cancels the export: The template was stopped after 60 seconds: its code ran too long (perhaps an endless loop).
null, undefinedRender as empty text, without stopping. Accessing something on undefined, however, throws an error, for example liste[$i].text when the list is too short. Hence ?? and ?.
Error in a commandStops the export. The dialog shows Report could not be created with the message
Unknown fieldsNot an error. The check warns. Under Field mapping in the template editor or on export you can redirect them to an existing field, see Map missing fields per project . Without a mapping the field stays empty
Old namesField and key names from before the switch to English names (p.projektname instead of p.name) stay empty. The check lists them under Notes, and Rewrite puts in today’s names
ScopeBody text, headers and footers. Commands belong directly in the text; embedded objects stay as they are
Large exportsWith one document per object, several processes in the browser render in parallel. A template with a loop over all objects renders in one pass

The check in the template editor finds the same errors before the export. It always reports FOR and IF blocks that are not closed and templates that cannot be read; it checks JavaScript errors with real sample data.

Further reading