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