diff --git a/EpsonTest.Templates/Templates/FinalReceipt.xml b/EpsonTest.Templates/Templates/FinalReceipt.xml index fdeabc0..fc19b2f 100644 --- a/EpsonTest.Templates/Templates/FinalReceipt.xml +++ b/EpsonTest.Templates/Templates/FinalReceipt.xml @@ -1,115 +1,116 @@ - {{CompanyName}} - {{Address1}} - {{Address2}} - {{Phone}} - - + {{CompanyName}} + {{Address1}} + {{Address2}} + {{Phone}} + + - - Debitorenrechnung - + + Debitorenrechnung + - - Guests: {{Guests}} - + + Guests: {{Guests}} + + - - - - - {{sub}} - - + + + + - {{sub}} + + - - --------- - + + --------- + - Summe: {{Total:F2}} {{Currency}} - + Summe: {{Total:F2}} {{Currency}} + - - {{TotalInAlternateCurrency:F2}} {{AlternateCurrency}} - - + + {{TotalInAlternateCurrency:F2}} {{AlternateCurrency}} + + - - - - + + + + - - {{sp.PaymentMethod}}: {{sp.Amount:F2}} {{sp.Currency}} - - - - + + {{sp.PaymentMethod}}: {{sp.Amount:F2}} {{sp.Currency}} + + + + - - + + - - - - MwSt % - Brutto - Netto - MwSt - - - - {{tax.Category}}:{{tax.Rate}}% - {{tax.Gross:F2}} {{tax.Currency}} - {{tax.Net:F2}} {{tax.Currency}} - {{tax.TaxAmount:F2}} {{tax.Currency}} - - + + + + MwSt % + Brutto + Netto + MwSt + + + + {{tax.Category}}:{{tax.Rate}}% + {{tax.Gross:F2}} {{tax.Currency}} + {{tax.Net:F2}} {{tax.Currency}} + {{tax.TaxAmount:F2}} {{tax.Currency}} + + - - Nicht mehrwertsteuerpflichtig - + + Nicht mehrwertsteuerpflichtig + - + - - - - - - - + + + + + + + - {{VatNumber}} - + {{VatNumber}} + - - {{tr.ReceiptType}} - {{tr.BookingType}} - {{tr.PaymentSystem}} - {{tr.TransactionNumber}} - - - - - - - - - - - - - + + {{tr.ReceiptType}} + {{tr.BookingType}} + {{tr.PaymentSystem}} + {{tr.TransactionNumber}} + + + + + + + + + + + + + - {{ThankYouMessage}} - {{GoodbyeMessageLine1}} - {{GoodbyeMessageLine2}} + {{ThankYouMessage}} + {{GoodbyeMessageLine1}} + {{GoodbyeMessageLine2}} - - - - - - Unterschrift - + + + + + + Unterschrift + diff --git a/Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs b/Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs index c411268..0637901 100644 --- a/Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs +++ b/Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs @@ -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 diff --git a/Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs b/Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs index 725924b..37247d1 100644 --- a/Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs +++ b/Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs @@ -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; + } + } } diff --git a/Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs b/Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs index eee7d20..9e969e5 100644 --- a/Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs +++ b/Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs @@ -109,6 +109,59 @@ public class LayoutEngine return string.Concat(parts); } + public List 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>(); + 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 { 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(); + for (int lineIdx = 0; lineIdx < maxLines; lineIdx++) + { + var parts = new List(); + 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 WrapText(string text, int maxWidth, int indent = 0) { var lines = new List(); @@ -177,20 +230,26 @@ public class LayoutEngine List<(string text, int width, string align)> columns, List> headerRows, List> dataRows, - int lineWidth) + int lineWidth, + List? wrapFlags = null) { var result = new List(); // 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) { - result.Add(FormatTableRow(row, colWidths, columns)); + if (hasWrap) + result.AddRange(FormatTableRows(row, colWidths, columns, wrapFlags!)); + else + result.Add(FormatTableRow(row, colWidths, columns)); } // Separator between header and data @@ -202,7 +261,10 @@ public class LayoutEngine // Data rows foreach (var row in dataRows) { - result.Add(FormatTableRow(row, colWidths, columns)); + if (hasWrap) + result.AddRange(FormatTableRows(row, colWidths, columns, wrapFlags!)); + else + result.Add(FormatTableRow(row, colWidths, columns)); } // Bottom border @@ -245,6 +307,40 @@ public class LayoutEngine return "|" + string.Join("|", parts) + "|"; } + private List FormatTableRows(List cells, List colWidths, + List<(string text, int width, string align)> columns, List wrapFlags) + { + // Wrap each cell that has wrap enabled + var allColLines = new List>(); + 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 { cell.Length > colWidths[i] ? cell[..colWidths[i]] : cell }); + } + + int maxLines = allColLines.Max(l => l.Count); + + var result = new List(); + for (int lineIdx = 0; lineIdx < maxLines; lineIdx++) + { + var parts = new List(); + 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) diff --git a/Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs b/Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs index eb3199a..2d86c6e 100644 --- a/Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs +++ b/Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs @@ -145,15 +145,36 @@ public class TemplateRenderer var evaluator = new ExpressionEvaluator(context); int effectiveWidth = row.Big ? _bigFontLineWidth : _lineWidth; - var columnData = row.Columns.Select(c => ( - text: evaluator.Evaluate(c.Text ?? ""), - width: c.Width ?? 0, - align: c.Align - )).ToList(); + bool anyWrap = row.Columns.Any(c => c.Wrap); - string text = _layout.FormatMultiColumn(columnData, effectiveWidth); - var cmd = CreateCommand(text, row.Bold, row.Big, row.Tall, row.Red, row.LineSpacing); - commands.Add(cmd); + if (anyWrap) + { + var columnData = row.Columns.Select(c => ( + text: evaluator.Evaluate(c.Text ?? ""), + 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(); + + string text = _layout.FormatMultiColumn(columnData, effectiveWidth); + var cmd = CreateCommand(text, row.Bold, row.Big, row.Tall, row.Red, row.LineSpacing); + commands.Add(cmd); + } } private void RenderSeparator(SeparatorNode separator, List 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) diff --git a/Inspectron.Epson.TemplateEngine/template_syntax.md b/Inspectron.Epson.TemplateEngine/template_syntax.md index ec77bde..06a89d2 100644 --- a/Inspectron.Epson.TemplateEngine/template_syntax.md +++ b/Inspectron.Epson.TemplateEngine/template_syntax.md @@ -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 + + + Name + Qty + Price + + MwSt % @@ -161,10 +169,28 @@ Multi-column layout with explicit column definitions. Each column is defined by {{tax.Net:F2}} {{tax.Currency}} {{tax.TaxAmount:F2}} {{tax.Currency}} + + + + Very Long Product Name Here + 12.50 + + + + + + {{item.Name}} + {{item.Description}} + ``` 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. + --- ### `` @@ -244,10 +270,11 @@ If `headerItems` is not set, the text content of `` 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| +--------------------+----------+ --> + + + + Name + Price +
+ ``` +When `wrap="true"` is set on a table ``, 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 | `` | _(root)_ | - | | `` | 1 PrintCommand (or N if wrapping) | `align`, `bold`, `big`, `tall`, `red`, `wrap`, `wrapIndent`, `lineSpacing` | | `` | 1 PrintCommand (or N if wrapping) | `left`, `right`, `bold`, `big`, `tall`, `red`, `wrap`, `wrapIndent`, `lineSpacing` | -| `` | 1 PrintCommand | `bold`, `big`, `tall`, `red`, `lineSpacing` + nested `` | +| `` | 1+ PrintCommand(s) (N if wrapping) | `bold`, `big`, `tall`, `red`, `lineSpacing` + nested `` | | `` | 1 PrintCommand | `char` | | `` | 1 PrintCommand (IsCut=true) | - | | `` | N PrintCommands (empty lines) | `lines` |