Files
Print_server/Inspectron.Epson.TemplateEngine/template_syntax.md
2026-02-03 14:27:13 +01:00

17 KiB

XML Receipt Template Syntax

Overview

Templates are XML documents that combine static text, data binding expressions, and control flow to produce a list of PrintCommand objects for Epson thermal receipt printers.

var engine = new ReceiptTemplateEngine();
List<PrintCommand> commands = engine.Render(
    xmlTemplate,      // XML template string
    jsonData,          // JSON data string
    lineWidth: 42,     // characters per line (normal font)
    bigFontLineWidth: 22  // characters per line (big font)
);

Root Element

<receipt>

Required root element. All other elements must be nested inside it.

<receipt>
  <!-- template content here -->
</receipt>

Content Elements

<line>

Outputs a single line of text. The most common element.

Attributes:

Attribute Type Default Description
align left / center / right left Text alignment within the line width
bold true / false false Bold text
big true / false false Double-width font (uses bigFontLineWidth for alignment)
tall true / false false Double-height font
red true / false false Red text (on supported printers)
lineSpacing int (none) Override line spacing in dots
wrap true / false false Word-wrap text that exceeds line width
wrapIndent int 0 Indent (in characters) for continuation lines when wrapping

Examples:

<!-- Simple text -->
<line>Hello World</line>

<!-- Empty line -->
<line />

<!-- Centered bold header -->
<line align="center" bold="true">RECEIPT</line>

<!-- Big red title -->
<line align="center" big="true" tall="true" red="true">{{Title}}</line>

<!-- Right-aligned -->
<line align="right">---------</line>

<!-- Long text with word wrap -->
<line wrap="true">{{dish.Number}}x {{dish.Name}}</line>

<!-- Wrap with indent for continuation lines -->
<line wrap="true" wrapIndent="4">1x Very Long Dish Name That Will Wrap To Next Line</line>
<!-- Output:
   1x Very Long Dish Name That
       Will Wrap To Next Line      -->

<!-- Custom line spacing -->
<line lineSpacing="50">Spaced out text</line>

When big="true", alignment uses bigFontLineWidth instead of lineWidth.


<columns>

Two-column layout: left-aligned left text, right-aligned right text. The line width is split in half.

Attributes:

Attribute Type Default Description
left string "" Left column content (supports {{}} expressions)
right string "" Right column content (supports {{}} expressions)
bold true / false false Bold text
big true / false false Double-width font
tall true / false false Double-height font
red true / false false Red text
lineSpacing int (none) Override line spacing
wrap true / false false Wrap left column if combined text exceeds line width
wrapIndent int 0 Indent for wrapped continuation lines

Examples:

<!-- Label-value pair -->
<columns left="Tisch:" right="{{TableNumber}}" />

<!-- Bold receipt info -->
<columns left="Rechnung Nr. {{ReceiptNumber}}" right="{{DateTime:HH:mm dd.MM.yyyy}}" bold="true" />

<!-- Price line with wrapping for long descriptions -->
<columns left="{{item.Quantity}}x {{item.Description}}" right="{{item.PriceDisplay}}" wrap="true" />

<!-- Payment line -->
<columns left="{{PaymentMethod}}" right="{{PaymentAmount:F2}} {{Currency}}" bold="true" />

Without wrap, the left column is padded to half the line width and the right column is right-padded to fill the remaining space. With wrap="true", if the combined text exceeds the line width, the left column wraps and the right column appears right-aligned on the first line.


<row>

Multi-column layout with explicit column definitions. Each column is defined by a nested <col> element.

Row attributes:

Attribute Type Default Description
bold true / false false Bold text
big true / false false Double-width font
tall true / false false Double-height font
red true / false false Red text
lineSpacing int (none) Override line spacing

<col> attributes:

Attribute Type Default Description
width int (auto) Column width in characters. Unspecified columns share remaining space equally.
align left / center / right left Text alignment within the column

Examples:

<!-- Tax breakdown header -->
<row>
  <col width="10" align="left">MwSt  %</col>
  <col width="10" align="right">Brutto</col>
  <col width="10" align="right">Netto</col>
  <col align="right">MwSt</col>
</row>

<!-- Tax breakdown data row -->
<row>
  <col width="10" align="left">{{tax.Category}}:{{tax.Rate}}%</col>
  <col width="10" align="right">{{tax.Gross:F2}} {{tax.Currency}}</col>
  <col width="10" align="right">{{tax.Net:F2}} {{tax.Currency}}</col>
  <col align="right">{{tax.TaxAmount:F2}} {{tax.Currency}}</col>
</row>

In this example, the first three columns are 10 characters wide. The fourth column gets all remaining space (lineWidth - 30).


<separator>

Outputs a line of repeated characters spanning the full line width.

Attributes:

Attribute Type Default Description
char single char - Character to repeat

Examples:

<!-- Default dashed line: ------------------------------------------ -->
<separator />

<!-- Asterisk line: ****************************************** -->
<separator char="*" />

<!-- Equals line: ========================================== -->
<separator char="=" />

Always uses lineWidth (not bigFontLineWidth).


<cut />

Triggers a paper cut. Produces a PrintCommand with IsCut = true.

<cut />

<feed>

Outputs one or more empty lines.

Attributes:

Attribute Type Default Description
lines int 1 Number of empty lines

Examples:

<!-- Single empty line (same as <line />) -->
<feed />

<!-- Three empty lines -->
<feed lines="3" />

<table>

Renders an ASCII box table with borders (+, -, |). Column definitions are provided via nested <col> elements.

Attributes:

Attribute Type Default Description
items string (none) JSON array path for data rows
var string "item" Loop variable name for each data row
headerItems string (none) JSON array path for header rows (optional)

If headerItems is not set, the text content of <col> elements is used as the header row. Columns without text produce no header.

<col> attributes:

Attribute Type Default Description
width int (auto) Column width in characters (excluding border characters)
align left / center / right left Cell content alignment

Column widths exclude border characters. With 3 columns, 4 border characters (|) are used, so the available content width is lineWidth - 4.

Examples:

<!-- Simple header-only table -->
<table>
  <col width="8" align="center">Order:</col>
  <col align="center">QR Info</col>
  <col width="12" align="center">Date</col>
</table>
<!-- Output:
+--------+------------------+------------+
| Order: |     QR Info      |    Date    |
+--------+------------------+------------+  -->

<!-- Table with data rows -->
<table items="Products" var="p">
  <col width="20" align="left">Name</col>
  <col width="10" align="right">Price</col>
</table>
<!-- Output:
+--------------------+----------+
|Name                |     Price|
+--------------------+----------+
|Margherita          |     12.50|
|Tiramisu            |      8.00|
+--------------------+----------+  -->

Control Flow

<foreach>

Iterates over a JSON array. For each item, the child elements are rendered with the loop variable available in the data context.

Attributes (both required):

Attribute Type Description
items string Path to the JSON array (supports dot notation for nested arrays)
var string Variable name to bind each array element to

Loop variables (available inside <foreach>):

Variable Type Description
{{$index}} int Zero-based index of the current item
{{$first}} bool true for the first item
{{$last}} bool true for the last item

Examples:

<!-- Simple list -->
<foreach items="Items" var="item">
  <line>{{item.Name}}: {{item.Price:F2}}</line>
</foreach>

<!-- Nested loops -->
<foreach items="Gangs" var="gang">
  <line big="true" red="true">{{gang.Id}}. {{gang.Name}}</line>
  <foreach items="gang.Dishes" var="dish">
    <line tall="true">{{dish.Number}}x {{dish.Name}}</line>
  </foreach>
</foreach>

<!-- Using $index -->
<foreach items="Items" var="item">
  <line>{{$index}}: {{item.Name}}</line>
</foreach>
<!-- Output:
   0: Pizza
   1: Pasta
   2: Salad  -->

<!-- Cut between gangs, but not after the last one -->
<foreach items="Gangs" var="gang">
  <line>{{gang.Name}}</line>
  <if test="!$last">
    <cut />
  </if>
</foreach>

If items resolves to null, an empty array, or a non-array value, the loop body is skipped entirely.


<if> / <else>

Conditionally renders child elements. An <else> block is optional and must immediately follow its corresponding <if>.

Attributes:

Attribute Type Description
test string Condition expression (see Condition Expressions below)

Examples:

<!-- Simple truthiness check -->
<if test="SpecialInstruction">
  <line bold="true">{{SpecialInstruction}}</line>
</if>

<!-- With else branch -->
<if test="DiscountInfo">
  <line>Discount: {{DiscountInfo.Description}}</line>
</if>
<else>
  <line>No discount applied</line>
</else>

<!-- Negated condition -->
<if test="!$last">
  <cut />
</if>

<!-- Comparison -->
<if test="Total>100">
  <line>Large order!</line>
</if>

<!-- String comparison -->
<if test="Status=='active'">
  <line>Active order</line>
</if>

<!-- Check nested property exists -->
<if test="dish.Modifications.HasModifications">
  <line>Modified</line>
</if>

<!-- Loop variable condition -->
<if test="$first">
  <line bold="true">First item header</line>
</if>

Data Binding

Expression Syntax {{}}

Expressions are enclosed in double curly braces and can appear in text content, left/right attributes of <columns>, and <col> text content.

Property access:

<!-- Simple property -->
<line>{{Name}}</line>

<!-- Nested property -->
<line>{{Person.Address.City}}</line>

<!-- Multiple expressions in one line -->
<line>{{FirstName}} {{LastName}}</line>

<!-- Mixed static text and expressions -->
<line>Order #{{OrderNumber}} - Table {{TableNumber}}</line>

<!-- Loop variable access -->
<foreach items="Items" var="item">
  <line>{{item.Name}} - {{item.Price}}</line>
</foreach>

Property lookup is case-insensitive. Both {{name}} and {{Name}} resolve the same JSON property.

If a property is missing or null, the expression resolves to an empty string.

Format Strings

Use a colon after the property name to apply a format string:

{{PropertyName:format}}

DateTime formatting (the JSON value must be an ISO 8601 date string):

<line>{{DateTime:dd.MM.yyyy}}</line>         <!-- 15.03.2024 -->
<line>{{DateTime:HH:mm:ss}}</line>           <!-- 14:30:00 -->
<line>{{DateTime:dd-MMM-yy HH:mm}}</line>    <!-- 15-Mar-24 14:30 -->

Number formatting (standard .NET format strings):

<line>{{Price:F2}}</line>                    <!-- 9.50 -->
<line>{{Amount:N0}}</line>                   <!-- 1,234 -->

Loop Variables

Available only inside <foreach> blocks:

<foreach items="Items" var="item">
  <line>{{$index}}: {{item.Name}}</line>     <!-- 0: Pizza -->
</foreach>
Variable Type Description
{{$index}} int Zero-based index
{{$first}} bool true on first iteration
{{$last}} bool true on last iteration

$first and $last are primarily useful in <if> conditions rather than text output.


Condition Expressions

Used in the test attribute of <if>. The following forms are supported:

Truthiness

A property path by itself checks whether the value is "truthy":

<if test="PropertyName">
JSON value Truthy?
"hello" yes
"" no
42 yes
0 no
true yes
false no
[1, 2] yes
[] no
null no
(missing) no
{ ... } yes

Negation

Prefix with ! to negate:

<if test="!PropertyName">    <!-- true when property is falsy or missing -->
<if test="!$last">           <!-- true when NOT the last loop iteration -->

Comparisons

Six comparison operators are supported. Operands can be property paths, numeric literals, or quoted string literals:

<if test="Count==5">          <!-- numeric equality -->
<if test="Count!=0">          <!-- numeric inequality -->
<if test="Count>3">           <!-- greater than -->
<if test="Count<10">          <!-- less than -->
<if test="Count>=1">          <!-- greater or equal -->
<if test="Count<=100">        <!-- less or equal -->
<if test="Status=='active'">  <!-- string equality (single quotes) -->

If both operands parse as numbers, numeric comparison is used. Otherwise, case-insensitive string comparison is used.

Comparisons can also be negated:

<if test="!Status=='inactive'">

Loop Variables in Conditions

<if test="$first">            <!-- first iteration -->
<if test="$last">             <!-- last iteration -->
<if test="!$last">            <!-- not the last iteration -->

Complete Example

Kitchen Receipt Template

<receipt>
  <line align="center" big="true" tall="true" red="true">{{Title}}</line>
  <separator />
  <line />

  <line align="center">{{TransactionDateTime:dd-MMM-yy HH:mm}} Nr.:{{ReceiptNumber}}</line>
  <line align="center">{{WaiterName}}</line>
  <line align="center" big="true" bold="true">Tisch: {{TableNumber}}</line>

  <if test="SpecialInstruction">
    <line />
    <line align="center" big="true" bold="true">{{SpecialInstruction}}</line>
    <line />
  </if>

  <separator />

  <foreach items="Gangs" var="gang">
    <line align="center" big="true" tall="true" red="true">{{gang.Id}}. {{gang.Name}}</line>
    <foreach items="gang.Dishes" var="dish">
      <line tall="true" wrap="true">{{dish.Number}}x {{dish.Name}}</line>
      <if test="dish.Comment">
        <line tall="true" bold="true">Comment: {{dish.Comment}}</line>
      </if>
    </foreach>
    <if test="!$last">
      <cut />
      <line />
      <line />
    </if>
  </foreach>

  <separator />
</receipt>

Sample JSON Data

{
  "Title": "Ristorante Bella",
  "TransactionDateTime": "2024-03-15T14:30:00Z",
  "ReceiptNumber": "42",
  "WaiterName": "Max Mustermann",
  "TableNumber": "5",
  "SpecialInstruction": null,
  "Gangs": [
    {
      "Id": 1,
      "Name": "Vorspeise",
      "Dishes": [
        { "Number": 2, "Name": "Caesar Salad", "Comment": null }
      ]
    },
    {
      "Id": 2,
      "Name": "Hauptgang",
      "Dishes": [
        { "Number": 1, "Name": "Wiener Schnitzel", "Comment": "Well done" }
      ]
    }
  ]
}

Printed Output (42 char width)

       Ristorante Bella
------------------------------------------

 15-Mar-24 14:30 Nr.:42
          Max Mustermann
     Tisch: 5
------------------------------------------
     1. Vorspeise
2x Caesar Salad

------------------------------------------
     2. Hauptgang
1x Wiener Schnitzel
Comment: Well done
------------------------------------------

Quick Reference

Element Produces Key Attributes
<receipt> (root) -
<line> 1 PrintCommand (or N if wrapping) align, bold, big, tall, red, wrap, wrapIndent, lineSpacing
<columns> 1 PrintCommand (or N if wrapping) left, right, bold, big, tall, red, wrap, wrapIndent, lineSpacing
<row> 1 PrintCommand bold, big, tall, red, lineSpacing + nested <col>
<separator> 1 PrintCommand char
<cut /> 1 PrintCommand (IsCut=true) -
<feed /> N PrintCommands (empty lines) lines
<foreach> N x children items (required), var (required)
<if> 0 or children test (required)
<else> 0 or children (must follow <if>)
<table> Multiple PrintCommands (bordered) items, var, headerItems + nested <col>