template engine
This commit is contained in:
275
Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs
Normal file
275
Inspectron.Epson.TemplateEngine/Rendering/LayoutEngine.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
namespace Inspectron.Epson.TemplateEngine.Rendering;
|
||||
|
||||
public class LayoutEngine
|
||||
{
|
||||
public string AlignText(string text, string alignment, int lineWidth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return text ?? "";
|
||||
|
||||
if (text.Length >= lineWidth)
|
||||
return text;
|
||||
|
||||
return alignment.ToLowerInvariant() switch
|
||||
{
|
||||
"center" => CenterText(text, lineWidth),
|
||||
"right" => text.PadLeft(lineWidth),
|
||||
_ => text // left-aligned is default (no padding)
|
||||
};
|
||||
}
|
||||
|
||||
private static string CenterText(string text, int lineWidth)
|
||||
{
|
||||
int totalPadding = lineWidth - text.Length;
|
||||
int leftPadding = totalPadding / 2;
|
||||
return new string(' ', leftPadding) + text;
|
||||
}
|
||||
|
||||
public string FormatTwoColumns(string left, string right, int lineWidth)
|
||||
{
|
||||
left ??= "";
|
||||
right ??= "";
|
||||
|
||||
int halfWidth = lineWidth / 2;
|
||||
return left.PadRight(halfWidth) + right.PadLeft(lineWidth - halfWidth);
|
||||
}
|
||||
|
||||
public List<string> FormatTwoColumnsWithWrap(string left, string right, int lineWidth)
|
||||
{
|
||||
left ??= "";
|
||||
right ??= "";
|
||||
|
||||
int halfWidth = lineWidth / 2;
|
||||
|
||||
// Try simple format first
|
||||
if (left.Length + right.Length <= lineWidth)
|
||||
{
|
||||
int spaces = lineWidth - left.Length - right.Length;
|
||||
if (spaces < 1) spaces = 1;
|
||||
return new List<string> { left + new string(' ', spaces) + right };
|
||||
}
|
||||
|
||||
// Wrap left side, right-align right on first line
|
||||
var leftLines = WrapText(left, halfWidth);
|
||||
var result = new List<string>();
|
||||
|
||||
for (int i = 0; i < leftLines.Count; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
int spaces = lineWidth - leftLines[i].Length - right.Length;
|
||||
if (spaces < 1)
|
||||
{
|
||||
result.Add(leftLines[i]);
|
||||
result.Add(right.PadLeft(lineWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(leftLines[i] + new string(' ', spaces) + right);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(leftLines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string FormatMultiColumn(List<(string text, int width, string align)> columns, int lineWidth)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
// Calculate widths: distribute remaining width among columns without explicit width
|
||||
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;
|
||||
|
||||
foreach (var (text, width, align) in columns)
|
||||
{
|
||||
int colWidth = width > 0 ? width : defaultWidth;
|
||||
if (colWidth <= 0) colWidth = 1;
|
||||
|
||||
string formatted = align.ToLowerInvariant() switch
|
||||
{
|
||||
"center" => CenterInWidth(text ?? "", colWidth),
|
||||
"right" => (text ?? "").PadLeft(colWidth),
|
||||
_ => (text ?? "").PadRight(colWidth)
|
||||
};
|
||||
|
||||
// Truncate if too long
|
||||
if (formatted.Length > colWidth)
|
||||
formatted = formatted[..colWidth];
|
||||
|
||||
parts.Add(formatted);
|
||||
}
|
||||
|
||||
return string.Concat(parts);
|
||||
}
|
||||
|
||||
public List<string> WrapText(string text, int maxWidth, int indent = 0)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
if (string.IsNullOrEmpty(text) || maxWidth <= 0)
|
||||
{
|
||||
lines.Add(text ?? "");
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (text.Length <= maxWidth)
|
||||
{
|
||||
lines.Add(text);
|
||||
return lines;
|
||||
}
|
||||
|
||||
var words = text.Split(' ');
|
||||
string currentLine = "";
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (var word in words)
|
||||
{
|
||||
int available = isFirst ? maxWidth : maxWidth - indent;
|
||||
if (available <= 0) available = 1;
|
||||
|
||||
if (currentLine.Length == 0)
|
||||
{
|
||||
currentLine = word;
|
||||
}
|
||||
else if (currentLine.Length + 1 + word.Length <= available)
|
||||
{
|
||||
currentLine += " " + word;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
lines.Add(currentLine);
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add(new string(' ', indent) + currentLine);
|
||||
}
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine.Length > 0)
|
||||
{
|
||||
if (isFirst)
|
||||
lines.Add(currentLine);
|
||||
else
|
||||
lines.Add(new string(' ', indent) + currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
public string CreateSeparator(char character, int lineWidth)
|
||||
{
|
||||
return new string(character, lineWidth);
|
||||
}
|
||||
|
||||
public List<string> FormatTable(
|
||||
List<(string text, int width, string align)> columns,
|
||||
List<List<string>> headerRows,
|
||||
List<List<string>> dataRows,
|
||||
int lineWidth)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
// Calculate column widths
|
||||
var colWidths = CalculateTableColumnWidths(columns, lineWidth);
|
||||
|
||||
// Top border
|
||||
result.Add(FormatTableBorder(colWidths));
|
||||
|
||||
// Header rows
|
||||
foreach (var row in headerRows)
|
||||
{
|
||||
result.Add(FormatTableRow(row, colWidths, columns));
|
||||
}
|
||||
|
||||
// Separator between header and data
|
||||
if (headerRows.Count > 0 && dataRows.Count > 0)
|
||||
{
|
||||
result.Add(FormatTableBorder(colWidths));
|
||||
}
|
||||
|
||||
// Data rows
|
||||
foreach (var row in dataRows)
|
||||
{
|
||||
result.Add(FormatTableRow(row, colWidths, columns));
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
result.Add(FormatTableBorder(colWidths));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<int> CalculateTableColumnWidths(List<(string text, int width, string align)> columns, int lineWidth)
|
||||
{
|
||||
// Account for borders: |col1|col2|col3| = columns.Count + 1 border chars
|
||||
int availableWidth = lineWidth - columns.Count - 1;
|
||||
int totalExplicit = columns.Where(c => c.width > 0).Sum(c => c.width);
|
||||
int unspecifiedCount = columns.Count(c => c.width <= 0);
|
||||
int remaining = availableWidth - totalExplicit;
|
||||
int defaultWidth = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
|
||||
|
||||
var widths = new List<int>();
|
||||
foreach (var (_, width, _) in columns)
|
||||
{
|
||||
widths.Add(width > 0 ? width : Math.Max(defaultWidth, 1));
|
||||
}
|
||||
return widths;
|
||||
}
|
||||
|
||||
private string FormatTableBorder(List<int> colWidths)
|
||||
{
|
||||
return "+" + string.Join("+", colWidths.Select(w => new string('-', w))) + "+";
|
||||
}
|
||||
|
||||
private string FormatTableRow(List<string> cells, List<int> colWidths, List<(string text, int width, string align)> columns)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int i = 0; i < colWidths.Count; i++)
|
||||
{
|
||||
string cell = i < cells.Count ? cells[i] : "";
|
||||
string align = i < columns.Count ? columns[i].align : "left";
|
||||
parts.Add(FormatCellContent(cell, colWidths[i], align));
|
||||
}
|
||||
return "|" + string.Join("|", parts) + "|";
|
||||
}
|
||||
|
||||
private string FormatCellContent(string text, int width, string align)
|
||||
{
|
||||
if (text.Length > width)
|
||||
text = text[..width];
|
||||
|
||||
return align.ToLowerInvariant() switch
|
||||
{
|
||||
"center" => CenterInWidth(text, width),
|
||||
"right" => text.PadLeft(width),
|
||||
_ => text.PadRight(width)
|
||||
};
|
||||
}
|
||||
|
||||
private static string CenterInWidth(string text, int width)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new string(' ', width);
|
||||
|
||||
if (text.Length >= width)
|
||||
return text[..width];
|
||||
|
||||
int totalPadding = width - text.Length;
|
||||
int leftPadding = totalPadding / 2;
|
||||
int rightPadding = totalPadding - leftPadding;
|
||||
|
||||
return new string(' ', leftPadding) + text + new string(' ', rightPadding);
|
||||
}
|
||||
}
|
||||
286
Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs
Normal file
286
Inspectron.Epson.TemplateEngine/Rendering/TemplateRenderer.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
using System.Text.Json;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
using Inspectron.Epson.TemplateEngine.Parsing;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Rendering;
|
||||
|
||||
public class TemplateRenderer
|
||||
{
|
||||
private readonly int _lineWidth;
|
||||
private readonly int _bigFontLineWidth;
|
||||
private readonly LayoutEngine _layout = new();
|
||||
|
||||
public TemplateRenderer(int lineWidth, int bigFontLineWidth)
|
||||
{
|
||||
_lineWidth = lineWidth;
|
||||
_bigFontLineWidth = bigFontLineWidth;
|
||||
}
|
||||
|
||||
public List<PrintCommand> Render(ReceiptNode receipt, DataContext context)
|
||||
{
|
||||
var commands = new List<PrintCommand>();
|
||||
RenderChildren(receipt.Children, context, commands);
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void RenderChildren(List<TemplateNode> children, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
var node = children[i];
|
||||
|
||||
if (node is IfNode ifNode)
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
bool condition = evaluator.EvaluateCondition(ifNode.Test);
|
||||
|
||||
if (condition)
|
||||
{
|
||||
RenderChildren(ifNode.Children, context, commands);
|
||||
// Skip following else
|
||||
if (i + 1 < children.Count && children[i + 1] is ElseNode)
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for following else
|
||||
if (i + 1 < children.Count && children[i + 1] is ElseNode elseNode)
|
||||
{
|
||||
RenderChildren(elseNode.Children, context, commands);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderNode(node, context, commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderNode(TemplateNode node, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case LineNode line:
|
||||
RenderLine(line, context, commands);
|
||||
break;
|
||||
case ColumnsNode columns:
|
||||
RenderColumns(columns, context, commands);
|
||||
break;
|
||||
case RowNode row:
|
||||
RenderRow(row, context, commands);
|
||||
break;
|
||||
case SeparatorNode separator:
|
||||
RenderSeparator(separator, commands);
|
||||
break;
|
||||
case CutNode:
|
||||
commands.Add(new PrintCommand("") { IsCut = true });
|
||||
break;
|
||||
case FeedNode feed:
|
||||
RenderFeed(feed, commands);
|
||||
break;
|
||||
case ForeachNode foreachNode:
|
||||
RenderForeach(foreachNode, context, commands);
|
||||
break;
|
||||
case TableNode table:
|
||||
RenderTable(table, context, commands);
|
||||
break;
|
||||
case ElseNode:
|
||||
// Handled by IfNode processing in RenderChildren
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderLine(LineNode line, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
string text = evaluator.Evaluate(line.Text ?? "");
|
||||
int effectiveWidth = line.Big ? _bigFontLineWidth : _lineWidth;
|
||||
|
||||
if (line.Wrap && text.Length > effectiveWidth)
|
||||
{
|
||||
var wrappedLines = _layout.WrapText(text, effectiveWidth, line.WrapIndent);
|
||||
foreach (var wrappedLine in wrappedLines)
|
||||
{
|
||||
var cmd = CreateCommand(wrappedLine, line.Bold, line.Big, line.Tall, line.Red, line.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text = _layout.AlignText(text, line.Align, effectiveWidth);
|
||||
var cmd = CreateCommand(text, line.Bold, line.Big, line.Tall, line.Red, line.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderColumns(ColumnsNode columns, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
string left = evaluator.Evaluate(columns.Left ?? "");
|
||||
string right = evaluator.Evaluate(columns.Right ?? "");
|
||||
int effectiveWidth = columns.Big ? _bigFontLineWidth : _lineWidth;
|
||||
|
||||
if (columns.Wrap)
|
||||
{
|
||||
var lines = _layout.FormatTwoColumnsWithWrap(left, right, effectiveWidth);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var cmd = CreateCommand(line, columns.Bold, columns.Big, columns.Tall, columns.Red, columns.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string text = _layout.FormatTwoColumns(left, right, effectiveWidth);
|
||||
var cmd = CreateCommand(text, columns.Bold, columns.Big, columns.Tall, columns.Red, columns.LineSpacing);
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderRow(RowNode row, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
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();
|
||||
|
||||
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<PrintCommand> commands)
|
||||
{
|
||||
string text = _layout.CreateSeparator(separator.Character, _lineWidth);
|
||||
commands.Add(new PrintCommand(text));
|
||||
}
|
||||
|
||||
private void RenderFeed(FeedNode feed, List<PrintCommand> commands)
|
||||
{
|
||||
for (int i = 0; i < feed.Lines; i++)
|
||||
{
|
||||
commands.Add(new PrintCommand(""));
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderForeach(ForeachNode foreachNode, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
|
||||
// Resolve the items collection
|
||||
var itemsElement = context.Resolve(foreachNode.Items);
|
||||
if (itemsElement == null || itemsElement.Value.ValueKind != JsonValueKind.Array)
|
||||
return;
|
||||
|
||||
var array = itemsElement.Value;
|
||||
int count = array.GetArrayLength();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = array[i];
|
||||
var childContext = context.CreateChildScope();
|
||||
childContext.SetVariable(foreachNode.Var, item);
|
||||
childContext.SetLoopVariable("$index", i);
|
||||
childContext.SetLoopVariable("$first", i == 0);
|
||||
childContext.SetLoopVariable("$last", i == count - 1);
|
||||
|
||||
RenderChildren(foreachNode.Children, childContext, commands);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTable(TableNode table, DataContext context, List<PrintCommand> commands)
|
||||
{
|
||||
var evaluator = new ExpressionEvaluator(context);
|
||||
|
||||
var columnDefs = table.Columns.Select(c => (
|
||||
text: evaluator.Evaluate(c.Text ?? ""),
|
||||
width: c.Width ?? 0,
|
||||
align: c.Align
|
||||
)).ToList();
|
||||
|
||||
// Header rows from headerItems or column text
|
||||
var headerRows = new List<List<string>>();
|
||||
if (!string.IsNullOrEmpty(table.HeaderItems))
|
||||
{
|
||||
var headerArray = context.Resolve(table.HeaderItems);
|
||||
if (headerArray != null && headerArray.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var headerItem in headerArray.Value.EnumerateArray())
|
||||
{
|
||||
var row = new List<string>();
|
||||
foreach (var col in table.Columns)
|
||||
{
|
||||
var childContext = context.CreateChildScope();
|
||||
childContext.SetVariable(table.Var ?? "item", headerItem);
|
||||
var childEval = new ExpressionEvaluator(childContext);
|
||||
row.Add(childEval.Evaluate(col.Text ?? ""));
|
||||
}
|
||||
headerRows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (columnDefs.Any(c => !string.IsNullOrEmpty(c.text)))
|
||||
{
|
||||
headerRows.Add(columnDefs.Select(c => c.text).ToList());
|
||||
}
|
||||
|
||||
// Data rows
|
||||
var dataRows = new List<List<string>>();
|
||||
if (!string.IsNullOrEmpty(table.Items))
|
||||
{
|
||||
var itemsArray = context.Resolve(table.Items);
|
||||
if (itemsArray != null && itemsArray.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var dataItem in itemsArray.Value.EnumerateArray())
|
||||
{
|
||||
var childContext = context.CreateChildScope();
|
||||
childContext.SetVariable(table.Var ?? "item", dataItem);
|
||||
var childEval = new ExpressionEvaluator(childContext);
|
||||
|
||||
var row = new List<string>();
|
||||
foreach (var child in table.Children)
|
||||
{
|
||||
if (child is LineNode lineChild)
|
||||
{
|
||||
row.Add(childEval.Evaluate(lineChild.Text ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
// If no line children, use column defs
|
||||
if (row.Count == 0)
|
||||
{
|
||||
foreach (var col in table.Columns)
|
||||
{
|
||||
row.Add(childEval.Evaluate(col.Text ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
dataRows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lines = _layout.FormatTable(columnDefs, headerRows, dataRows, _lineWidth);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
commands.Add(new PrintCommand(line));
|
||||
}
|
||||
}
|
||||
|
||||
private static PrintCommand CreateCommand(string text, bool bold, bool big, bool tall, bool red, int? lineSpacing)
|
||||
{
|
||||
return new PrintCommand(text, isBig: big, isBold: bold)
|
||||
{
|
||||
IsTall = tall,
|
||||
IsRed = red,
|
||||
SetLineSpacing = lineSpacing
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user