templates tested
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<line>Guests: {{Guests}}</line>
|
||||
<line />
|
||||
|
||||
|
||||
<foreach items="Items" var="item">
|
||||
<columns left="{{item.Quantity}}x {{item.Description}}" right="{{item.PriceDisplay}}" wrap="true" />
|
||||
<foreach items="item.SubItems" var="sub">
|
||||
@@ -51,17 +52,17 @@
|
||||
<foreach items="TaxBreakdown" var="tax">
|
||||
<if test="$first">
|
||||
<row>
|
||||
<col width="11" align="left">MwSt %</col>
|
||||
<col width="11" align="right">Brutto</col>
|
||||
<col width="11" align="right">Netto</col>
|
||||
<col align="right">MwSt</col>
|
||||
<col width="25%" align="left">MwSt %</col>
|
||||
<col width="25%" align="right" wrap="true" >Brutto</col>
|
||||
<col width="25%" align="right" wrap="true">Netto</col>
|
||||
<col width="25%" align="right">MwSt</col>
|
||||
</row>
|
||||
</if>
|
||||
<row>
|
||||
<col width="11" align="left">{{tax.Category}}:{{tax.Rate}}%</col>
|
||||
<col width="11" align="right"> {{tax.Gross:F2}} {{tax.Currency}}</col>
|
||||
<col width="11" align="right"> {{tax.Net:F2}} {{tax.Currency}}</col>
|
||||
<col align="right"> {{tax.TaxAmount:F2}} {{tax.Currency}}</col>
|
||||
<col width="25%" align="left">{{tax.Category}}:{{tax.Rate}}%</col>
|
||||
<col width="25%" align="right"> {{tax.Gross:F2}} {{tax.Currency}}</col>
|
||||
<col width="25%" align="right"> {{tax.Net:F2}} {{tax.Currency}}</col>
|
||||
<col width="25%" align="right"> {{tax.TaxAmount:F2}} {{tax.Currency}}</col>
|
||||
</row>
|
||||
</foreach>
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ public class ColumnDef
|
||||
{
|
||||
public string? Text { get; set; }
|
||||
public int? Width { get; set; }
|
||||
public double? WidthPercent { get; set; }
|
||||
public string Align { get; set; } = "left";
|
||||
public bool Wrap { get; set; }
|
||||
}
|
||||
|
||||
public class SeparatorNode : TemplateNode
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Parsing;
|
||||
@@ -92,9 +93,10 @@ public class TemplateParser
|
||||
var col = new ColumnDef
|
||||
{
|
||||
Text = GetTextContent(colElement),
|
||||
Width = GetNullableIntAttr(colElement, "width"),
|
||||
Align = GetAttr(colElement, "align", "left")
|
||||
Align = GetAttr(colElement, "align", "left"),
|
||||
Wrap = GetBoolAttr(colElement, "wrap")
|
||||
};
|
||||
ParseColumnWidth(colElement, col);
|
||||
node.Columns.Add(col);
|
||||
}
|
||||
|
||||
@@ -158,9 +160,10 @@ public class TemplateParser
|
||||
var col = new ColumnDef
|
||||
{
|
||||
Text = GetTextContent(colElement),
|
||||
Width = GetNullableIntAttr(colElement, "width"),
|
||||
Align = GetAttr(colElement, "align", "left")
|
||||
Align = GetAttr(colElement, "align", "left"),
|
||||
Wrap = GetBoolAttr(colElement, "wrap")
|
||||
};
|
||||
ParseColumnWidth(colElement, col);
|
||||
node.Columns.Add(col);
|
||||
}
|
||||
|
||||
@@ -247,4 +250,22 @@ public class TemplateParser
|
||||
if (attr == null) return null;
|
||||
return int.TryParse(attr.Value, out var v) ? v : null;
|
||||
}
|
||||
|
||||
private static void ParseColumnWidth(XElement element, ColumnDef col)
|
||||
{
|
||||
var attr = element.Attribute("width");
|
||||
if (attr == null) return;
|
||||
|
||||
var value = attr.Value.Trim();
|
||||
if (value.EndsWith('%'))
|
||||
{
|
||||
if (double.TryParse(value[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var pct))
|
||||
col.WidthPercent = pct;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(value, out var w))
|
||||
col.Width = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,59 @@ public class LayoutEngine
|
||||
return string.Concat(parts);
|
||||
}
|
||||
|
||||
public List<string> FormatMultiColumnWithWrap(
|
||||
List<(string text, int width, string align, bool wrap)> columns, int lineWidth)
|
||||
{
|
||||
// Calculate widths
|
||||
int totalExplicit = columns.Where(c => c.width > 0).Sum(c => c.width);
|
||||
int unspecifiedCount = columns.Count(c => c.width <= 0);
|
||||
int remaining = lineWidth - totalExplicit;
|
||||
int defaultWidth = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
|
||||
|
||||
var colWidths = columns.Select(c => c.width > 0 ? c.width : Math.Max(defaultWidth, 1)).ToList();
|
||||
|
||||
// Wrap or truncate each column's text into lines
|
||||
var allColLines = new List<List<string>>();
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var (text, _, _, wrap) = columns[i];
|
||||
text ??= "";
|
||||
int w = colWidths[i];
|
||||
|
||||
if (wrap)
|
||||
allColLines.Add(WrapText(text, w));
|
||||
else
|
||||
allColLines.Add(new List<string> { text.Length > w ? text[..w] : text });
|
||||
}
|
||||
|
||||
// Find the tallest column
|
||||
int maxLines = allColLines.Max(l => l.Count);
|
||||
|
||||
// Build each output line
|
||||
var result = new List<string>();
|
||||
for (int lineIdx = 0; lineIdx < maxLines; lineIdx++)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int colIdx = 0; colIdx < columns.Count; colIdx++)
|
||||
{
|
||||
string cellText = lineIdx < allColLines[colIdx].Count ? allColLines[colIdx][lineIdx] : "";
|
||||
int w = colWidths[colIdx];
|
||||
string formatted = columns[colIdx].align.ToLowerInvariant() switch
|
||||
{
|
||||
"center" => CenterInWidth(cellText, w),
|
||||
"right" => cellText.PadLeft(w),
|
||||
_ => cellText.PadRight(w)
|
||||
};
|
||||
if (formatted.Length > w)
|
||||
formatted = formatted[..w];
|
||||
parts.Add(formatted);
|
||||
}
|
||||
result.Add(string.Concat(parts));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<string> WrapText(string text, int maxWidth, int indent = 0)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
@@ -177,19 +230,25 @@ public class LayoutEngine
|
||||
List<(string text, int width, string align)> columns,
|
||||
List<List<string>> headerRows,
|
||||
List<List<string>> dataRows,
|
||||
int lineWidth)
|
||||
int lineWidth,
|
||||
List<bool>? wrapFlags = null)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
// Calculate column widths
|
||||
var colWidths = CalculateTableColumnWidths(columns, lineWidth);
|
||||
|
||||
bool hasWrap = wrapFlags != null && wrapFlags.Any(w => w);
|
||||
|
||||
// Top border
|
||||
result.Add(FormatTableBorder(colWidths));
|
||||
|
||||
// Header rows
|
||||
foreach (var row in headerRows)
|
||||
{
|
||||
if (hasWrap)
|
||||
result.AddRange(FormatTableRows(row, colWidths, columns, wrapFlags!));
|
||||
else
|
||||
result.Add(FormatTableRow(row, colWidths, columns));
|
||||
}
|
||||
|
||||
@@ -202,6 +261,9 @@ public class LayoutEngine
|
||||
// Data rows
|
||||
foreach (var row in dataRows)
|
||||
{
|
||||
if (hasWrap)
|
||||
result.AddRange(FormatTableRows(row, colWidths, columns, wrapFlags!));
|
||||
else
|
||||
result.Add(FormatTableRow(row, colWidths, columns));
|
||||
}
|
||||
|
||||
@@ -245,6 +307,40 @@ public class LayoutEngine
|
||||
return "|" + string.Join("|", parts) + "|";
|
||||
}
|
||||
|
||||
private List<string> FormatTableRows(List<string> cells, List<int> colWidths,
|
||||
List<(string text, int width, string align)> columns, List<bool> wrapFlags)
|
||||
{
|
||||
// Wrap each cell that has wrap enabled
|
||||
var allColLines = new List<List<string>>();
|
||||
for (int i = 0; i < colWidths.Count; i++)
|
||||
{
|
||||
string cell = i < cells.Count ? cells[i] : "";
|
||||
bool wrap = i < wrapFlags.Count && wrapFlags[i];
|
||||
|
||||
if (wrap)
|
||||
allColLines.Add(WrapText(cell, colWidths[i]));
|
||||
else
|
||||
allColLines.Add(new List<string> { cell.Length > colWidths[i] ? cell[..colWidths[i]] : cell });
|
||||
}
|
||||
|
||||
int maxLines = allColLines.Max(l => l.Count);
|
||||
|
||||
var result = new List<string>();
|
||||
for (int lineIdx = 0; lineIdx < maxLines; lineIdx++)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int colIdx = 0; colIdx < colWidths.Count; colIdx++)
|
||||
{
|
||||
string cellText = lineIdx < allColLines[colIdx].Count ? allColLines[colIdx][lineIdx] : "";
|
||||
string align = colIdx < columns.Count ? columns[colIdx].align : "left";
|
||||
parts.Add(FormatCellContent(cellText, colWidths[colIdx], align));
|
||||
}
|
||||
result.Add("|" + string.Join("|", parts) + "|");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string FormatCellContent(string text, int width, string align)
|
||||
{
|
||||
if (text.Length > width)
|
||||
|
||||
@@ -145,9 +145,29 @@ public class TemplateRenderer
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
int effectiveWidth = row.Big ? _bigFontLineWidth : _lineWidth;
|
||||
|
||||
bool anyWrap = row.Columns.Any(c => c.Wrap);
|
||||
|
||||
if (anyWrap)
|
||||
{
|
||||
var columnData = row.Columns.Select(c => (
|
||||
text: evaluator.Evaluate(c.Text ?? ""),
|
||||
width: c.Width ?? 0,
|
||||
width: ResolveColumnWidth(c, effectiveWidth),
|
||||
align: c.Align,
|
||||
wrap: c.Wrap
|
||||
)).ToList();
|
||||
|
||||
var lines = _layout.FormatMultiColumnWithWrap(columnData, effectiveWidth);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var cmd = CreateCommand(line, row.Bold, row.Big, row.Tall, row.Red, row.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var columnData = row.Columns.Select(c => (
|
||||
text: evaluator.Evaluate(c.Text ?? ""),
|
||||
width: ResolveColumnWidth(c, effectiveWidth),
|
||||
align: c.Align
|
||||
)).ToList();
|
||||
|
||||
@@ -155,6 +175,7 @@ public class TemplateRenderer
|
||||
var cmd = CreateCommand(text, row.Bold, row.Big, row.Tall, row.Red, row.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderSeparator(SeparatorNode separator, List<PrintCommand> commands)
|
||||
{
|
||||
@@ -199,9 +220,10 @@ public class TemplateRenderer
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
|
||||
int tableContentWidth = _lineWidth - table.Columns.Count - 1;
|
||||
var columnDefs = table.Columns.Select(c => (
|
||||
text: evaluator.Evaluate(c.Text ?? ""),
|
||||
width: c.Width ?? 0,
|
||||
width: ResolveColumnWidth(c, tableContentWidth),
|
||||
align: c.Align
|
||||
)).ToList();
|
||||
|
||||
@@ -267,13 +289,21 @@ public class TemplateRenderer
|
||||
}
|
||||
}
|
||||
|
||||
var lines = _layout.FormatTable(columnDefs, headerRows, dataRows, _lineWidth);
|
||||
var wrapFlags = table.Columns.Select(c => c.Wrap).ToList();
|
||||
var lines = _layout.FormatTable(columnDefs, headerRows, dataRows, _lineWidth, wrapFlags);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
commands.Add(new PrintCommand(line));
|
||||
}
|
||||
}
|
||||
|
||||
private static int ResolveColumnWidth(ColumnDef col, int availableWidth)
|
||||
{
|
||||
if (col.Width.HasValue) return col.Width.Value;
|
||||
if (col.WidthPercent.HasValue) return (int)(availableWidth * col.WidthPercent.Value / 100.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static PrintCommand CreateCommand(string text, bool bold, bool big, bool tall, bool red, int? lineSpacing)
|
||||
{
|
||||
return new PrintCommand(text, isBig: big, isBold: bold)
|
||||
|
||||
@@ -140,12 +140,20 @@ Multi-column layout with explicit column definitions. Each column is defined by
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `width` | int | _(auto)_ | Column width in characters. Unspecified columns share remaining space equally. |
|
||||
| `width` | int or string | _(auto)_ | Column width as a character count (`"10"`) or percentage of the row width (`"33.3%"`). Unspecified columns share remaining space equally. |
|
||||
| `align` | `left` / `center` / `right` | `left` | Text alignment within the column |
|
||||
| `wrap` | `true` / `false` | `false` | Word-wrap text that exceeds column width. All columns expand vertically to match the tallest cell. |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```xml
|
||||
<!-- Equal-width columns using percentages -->
|
||||
<row>
|
||||
<col width="33.3%" align="left">Name</col>
|
||||
<col width="33.3%" align="right">Qty</col>
|
||||
<col width="33.3%" align="right">Price</col>
|
||||
</row>
|
||||
|
||||
<!-- Tax breakdown header -->
|
||||
<row>
|
||||
<col width="10" align="left">MwSt %</col>
|
||||
@@ -161,10 +169,28 @@ Multi-column layout with explicit column definitions. Each column is defined by
|
||||
<col width="10" align="right">{{tax.Net:F2}} {{tax.Currency}}</col>
|
||||
<col align="right">{{tax.TaxAmount:F2}} {{tax.Currency}}</col>
|
||||
</row>
|
||||
|
||||
<!-- Row with word wrap on the first column -->
|
||||
<row>
|
||||
<col width="10" align="left" wrap="true">Very Long Product Name Here</col>
|
||||
<col align="right">12.50</col>
|
||||
</row>
|
||||
<!-- Output (42-char width, col2 auto-fills remaining 32 chars):
|
||||
Very Long 12.50
|
||||
Product
|
||||
Name Here -->
|
||||
|
||||
<!-- Both columns wrapping -->
|
||||
<row>
|
||||
<col width="20" align="left" wrap="true">{{item.Name}}</col>
|
||||
<col width="22" align="left" wrap="true">{{item.Description}}</col>
|
||||
</row>
|
||||
```
|
||||
|
||||
In this example, the first three columns are 10 characters wide. The fourth column gets all remaining space (`lineWidth - 30`).
|
||||
|
||||
When `wrap="true"` is set on a column, text that exceeds the column width wraps to multiple lines. All columns in the row expand vertically to match the tallest cell. Each wrapped line respects the column's `align` attribute.
|
||||
|
||||
---
|
||||
|
||||
### `<separator>`
|
||||
@@ -244,10 +270,11 @@ If `headerItems` is not set, the text content of `<col>` elements is used as the
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `width` | int | _(auto)_ | Column width in characters (excluding border characters) |
|
||||
| `width` | int or string | _(auto)_ | Column width as a character count (`"10"`) or percentage of the content area (`"50%"`). Percentages are relative to the content width (line width minus border characters). Unspecified columns share remaining space equally. |
|
||||
| `align` | `left` / `center` / `right` | `left` | Cell content alignment |
|
||||
| `wrap` | `true` / `false` | `false` | Word-wrap cell text that exceeds column width. All columns in the row expand vertically to match the tallest cell. |
|
||||
|
||||
Column widths exclude border characters. With 3 columns, 4 border characters (`|`) are used, so the available content width is `lineWidth - 4`.
|
||||
Column widths exclude border characters. With 3 columns, 4 border characters (`|`) are used, so the available content width is `lineWidth - 4`. Percentage widths are calculated against this content width.
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -275,8 +302,24 @@ Column widths exclude border characters. With 3 columns, 4 border characters (`|
|
||||
|Margherita | 12.50|
|
||||
|Tiramisu | 8.00|
|
||||
+--------------------+----------+ -->
|
||||
|
||||
<!-- Table with word wrap -->
|
||||
<table items="Products" var="p">
|
||||
<col width="12" align="left" wrap="true">Name</col>
|
||||
<col width="8" align="right">Price</col>
|
||||
</table>
|
||||
<!-- Output (cell text wraps, all columns expand vertically):
|
||||
+------------+--------+
|
||||
|Name | Price|
|
||||
+------------+--------+
|
||||
|Margherita | 12.50|
|
||||
|Wiener | 8.00|
|
||||
|Schnitzel | |
|
||||
+------------+--------+ -->
|
||||
```
|
||||
|
||||
When `wrap="true"` is set on a table `<col>`, cell text that exceeds the column width wraps to multiple lines. All columns in the row expand vertically to match the tallest cell, with empty cells padded with spaces. Each wrapped line respects the column's `align` attribute.
|
||||
|
||||
---
|
||||
|
||||
## Control Flow
|
||||
@@ -632,7 +675,7 @@ Comment: Well done
|
||||
| `<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>` |
|
||||
| `<row>` | 1+ PrintCommand(s) (N if wrapping) | `bold`, `big`, `tall`, `red`, `lineSpacing` + nested `<col>` |
|
||||
| `<separator>` | 1 PrintCommand | `char` |
|
||||
| `<cut />` | 1 PrintCommand (IsCut=true) | - |
|
||||
| `<feed />` | N PrintCommands (empty lines) | `lines` |
|
||||
|
||||
Reference in New Issue
Block a user