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

643 lines
17 KiB
Markdown

# 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.
```csharp
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.
```xml
<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:**
```xml
<!-- 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:**
```xml
<!-- 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:**
```xml
<!-- 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:**
```xml
<!-- 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`.
```xml
<cut />
```
---
### `<feed>`
Outputs one or more empty lines.
**Attributes:**
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `lines` | int | `1` | Number of empty lines |
**Examples:**
```xml
<!-- 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:**
```xml
<!-- 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:**
```xml
<!-- 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:**
```xml
<!-- 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:**
```xml
<!-- 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):
```xml
<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):
```xml
<line>{{Price:F2}}</line> <!-- 9.50 -->
<line>{{Amount:N0}}</line> <!-- 1,234 -->
```
### Loop Variables
Available only inside `<foreach>` blocks:
```xml
<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":
```xml
<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:
```xml
<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:
```xml
<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:
```xml
<if test="!Status=='inactive'">
```
### Loop Variables in Conditions
```xml
<if test="$first"> <!-- first iteration -->
<if test="$last"> <!-- last iteration -->
<if test="!$last"> <!-- not the last iteration -->
```
---
## Complete Example
### Kitchen Receipt Template
```xml
<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
```json
{
"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>` |