template engine
This commit is contained in:
123
Inspectron.Epson.TemplateEngine/DataBinding/DataContext.cs
Normal file
123
Inspectron.Epson.TemplateEngine/DataBinding/DataContext.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
|
||||
public class DataContext
|
||||
{
|
||||
private readonly JsonElement _root;
|
||||
private readonly DataContext? _parent;
|
||||
private readonly Dictionary<string, JsonElement> _variables = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, object> _loopVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public DataContext(JsonElement root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
private DataContext(JsonElement root, DataContext parent)
|
||||
{
|
||||
_root = root;
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public DataContext CreateChildScope()
|
||||
{
|
||||
return new DataContext(_root, this);
|
||||
}
|
||||
|
||||
public void SetVariable(string name, JsonElement value)
|
||||
{
|
||||
_variables[name] = value;
|
||||
}
|
||||
|
||||
public void SetLoopVariable(string name, object value)
|
||||
{
|
||||
_loopVariables[name] = value;
|
||||
}
|
||||
|
||||
public object? GetLoopVariable(string name)
|
||||
{
|
||||
if (_loopVariables.TryGetValue(name, out var value))
|
||||
return value;
|
||||
return _parent?.GetLoopVariable(name);
|
||||
}
|
||||
|
||||
public JsonElement? Resolve(string expression)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
return null;
|
||||
|
||||
var parts = expression.Split('.');
|
||||
var firstPart = parts[0];
|
||||
|
||||
// Check local variables first
|
||||
JsonElement? current = null;
|
||||
|
||||
if (_variables.TryGetValue(firstPart, out var varValue))
|
||||
{
|
||||
current = varValue;
|
||||
}
|
||||
else if (_parent != null)
|
||||
{
|
||||
// Walk up scope chain for variables
|
||||
var ctx = _parent;
|
||||
while (ctx != null)
|
||||
{
|
||||
if (ctx._variables.TryGetValue(firstPart, out var parentVar))
|
||||
{
|
||||
current = parentVar;
|
||||
break;
|
||||
}
|
||||
ctx = ctx._parent;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in variables, try root
|
||||
if (current == null)
|
||||
{
|
||||
current = GetProperty(_root, firstPart);
|
||||
}
|
||||
|
||||
if (current == null)
|
||||
return null;
|
||||
|
||||
// Navigate remaining parts
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
current = GetProperty(current.Value, parts[i]);
|
||||
if (current == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static JsonElement? GetProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
// Case-insensitive property lookup
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase))
|
||||
return prop.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetStringValue(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString() ?? "",
|
||||
JsonValueKind.Number => element.TryGetInt64(out var l) ? l.ToString() : element.GetDouble().ToString(),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
JsonValueKind.Null => "",
|
||||
JsonValueKind.Undefined => "",
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
|
||||
public class ExpressionEvaluator
|
||||
{
|
||||
private static readonly Regex ExpressionPattern = new(@"\{\{(.+?)\}\}", RegexOptions.Compiled);
|
||||
|
||||
private readonly DataContext _context;
|
||||
|
||||
public ExpressionEvaluator(DataContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public string Evaluate(string template)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
return "";
|
||||
|
||||
return ExpressionPattern.Replace(template, match =>
|
||||
{
|
||||
var expression = match.Groups[1].Value.Trim();
|
||||
return ResolveExpression(expression);
|
||||
});
|
||||
}
|
||||
|
||||
private string ResolveExpression(string expression)
|
||||
{
|
||||
// Loop variables: $index, $first, $last
|
||||
if (expression.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(expression);
|
||||
return loopVar?.ToString() ?? "";
|
||||
}
|
||||
|
||||
// Format string: PropertyName:format
|
||||
string? format = null;
|
||||
var colonIndex = expression.IndexOf(':');
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
format = expression[(colonIndex + 1)..];
|
||||
expression = expression[..colonIndex];
|
||||
}
|
||||
|
||||
var element = _context.Resolve(expression);
|
||||
if (element == null)
|
||||
return "";
|
||||
|
||||
if (format != null)
|
||||
return FormatValue(element.Value, format);
|
||||
|
||||
return DataContext.GetStringValue(element.Value);
|
||||
}
|
||||
|
||||
private static string FormatValue(JsonElement element, string format)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
if (str != null && DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt))
|
||||
{
|
||||
return dt.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
return str ?? "";
|
||||
}
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (element.TryGetDecimal(out var d))
|
||||
return d.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return DataContext.GetStringValue(element);
|
||||
}
|
||||
|
||||
public bool EvaluateCondition(string test)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(test))
|
||||
return false;
|
||||
|
||||
// Negation
|
||||
bool negate = false;
|
||||
var expr = test.Trim();
|
||||
if (expr.StartsWith("!"))
|
||||
{
|
||||
negate = true;
|
||||
expr = expr[1..].Trim();
|
||||
}
|
||||
|
||||
// Loop variables: $first, $last
|
||||
if (expr.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(expr);
|
||||
bool loopResult = loopVar is bool b ? b : loopVar != null;
|
||||
return negate ? !loopResult : loopResult;
|
||||
}
|
||||
|
||||
// Comparison operators
|
||||
var comparisonOps = new[] { "==", "!=", ">=", "<=", ">", "<" };
|
||||
foreach (var op in comparisonOps)
|
||||
{
|
||||
var parts = SplitComparison(expr, op);
|
||||
if (parts != null)
|
||||
{
|
||||
bool compResult = EvaluateComparison(parts.Value.left, op, parts.Value.right);
|
||||
return negate ? !compResult : compResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Truthiness check - property exists and has a value
|
||||
var result = IsTruthy(expr);
|
||||
return negate ? !result : result;
|
||||
}
|
||||
|
||||
private (string left, string right)? SplitComparison(string expr, string op)
|
||||
{
|
||||
var idx = expr.IndexOf(op, StringComparison.Ordinal);
|
||||
if (idx < 0) return null;
|
||||
|
||||
// Make sure we don't confuse == with = or != with !
|
||||
if (op == "=" && idx > 0 && expr[idx - 1] == '!') return null;
|
||||
if (op == ">" && idx > 0 && (expr[idx - 1] == '>' || expr[idx - 1] == '<')) return null;
|
||||
|
||||
var left = expr[..idx].Trim();
|
||||
var right = expr[(idx + op.Length)..].Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right))
|
||||
return null;
|
||||
|
||||
return (left, right);
|
||||
}
|
||||
|
||||
private bool EvaluateComparison(string left, string op, string right)
|
||||
{
|
||||
var leftVal = ResolveComparisonValue(left);
|
||||
var rightVal = ResolveComparisonValue(right);
|
||||
|
||||
// Try numeric comparison
|
||||
if (decimal.TryParse(leftVal, CultureInfo.InvariantCulture, out var leftNum)
|
||||
&& decimal.TryParse(rightVal, CultureInfo.InvariantCulture, out var rightNum))
|
||||
{
|
||||
return op switch
|
||||
{
|
||||
"==" => leftNum == rightNum,
|
||||
"!=" => leftNum != rightNum,
|
||||
">" => leftNum > rightNum,
|
||||
"<" => leftNum < rightNum,
|
||||
">=" => leftNum >= rightNum,
|
||||
"<=" => leftNum <= rightNum,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
// String comparison
|
||||
int cmp = string.Compare(leftVal, rightVal, StringComparison.OrdinalIgnoreCase);
|
||||
return op switch
|
||||
{
|
||||
"==" => cmp == 0,
|
||||
"!=" => cmp != 0,
|
||||
">" => cmp > 0,
|
||||
"<" => cmp < 0,
|
||||
">=" => cmp >= 0,
|
||||
"<=" => cmp <= 0,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveComparisonValue(string value)
|
||||
{
|
||||
// Quoted string literal
|
||||
if ((value.StartsWith("'") && value.EndsWith("'")) ||
|
||||
(value.StartsWith("\"") && value.EndsWith("\"")))
|
||||
{
|
||||
return value[1..^1];
|
||||
}
|
||||
|
||||
// Numeric literal
|
||||
if (decimal.TryParse(value, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Loop variable
|
||||
if (value.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(value);
|
||||
return loopVar?.ToString() ?? "";
|
||||
}
|
||||
|
||||
// Property resolution
|
||||
var element = _context.Resolve(value);
|
||||
if (element == null)
|
||||
return "";
|
||||
|
||||
return DataContext.GetStringValue(element.Value);
|
||||
}
|
||||
|
||||
private bool IsTruthy(string expression)
|
||||
{
|
||||
var element = _context.Resolve(expression);
|
||||
if (element == null)
|
||||
return false;
|
||||
|
||||
return element.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Null => false,
|
||||
JsonValueKind.Undefined => false,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.String => !string.IsNullOrEmpty(element.Value.GetString()),
|
||||
JsonValueKind.Number => element.Value.GetDouble() != 0,
|
||||
JsonValueKind.Array => element.Value.GetArrayLength() > 0,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
13
Inspectron.Epson.TemplateEngine/Exceptions.cs
Normal file
13
Inspectron.Epson.TemplateEngine/Exceptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Inspectron.Epson.TemplateEngine;
|
||||
|
||||
public class TemplateParsingException : Exception
|
||||
{
|
||||
public TemplateParsingException(string message) : base(message) { }
|
||||
public TemplateParsingException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class TemplateRenderingException : Exception
|
||||
{
|
||||
public TemplateRenderingException(string message) : base(message) { }
|
||||
public TemplateRenderingException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Inspectron.Epson\Inspectron.Epson.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
84
Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs
Normal file
84
Inspectron.Epson.TemplateEngine/Parsing/TemplateNode.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace Inspectron.Epson.TemplateEngine.Parsing;
|
||||
|
||||
public abstract class TemplateNode
|
||||
{
|
||||
public List<TemplateNode> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ReceiptNode : TemplateNode { }
|
||||
|
||||
public class LineNode : TemplateNode
|
||||
{
|
||||
public string? Text { get; set; }
|
||||
public bool Bold { get; set; }
|
||||
public bool Big { get; set; }
|
||||
public bool Tall { get; set; }
|
||||
public bool Red { get; set; }
|
||||
public string Align { get; set; } = "left";
|
||||
public int? LineSpacing { get; set; }
|
||||
public bool Wrap { get; set; }
|
||||
public int WrapIndent { get; set; }
|
||||
}
|
||||
|
||||
public class ColumnsNode : TemplateNode
|
||||
{
|
||||
public string? Left { get; set; }
|
||||
public string? Right { get; set; }
|
||||
public bool Bold { get; set; }
|
||||
public bool Big { get; set; }
|
||||
public bool Tall { get; set; }
|
||||
public bool Red { get; set; }
|
||||
public int? LineSpacing { get; set; }
|
||||
public bool Wrap { get; set; }
|
||||
public int WrapIndent { get; set; }
|
||||
}
|
||||
|
||||
public class RowNode : TemplateNode
|
||||
{
|
||||
public List<ColumnDef> Columns { get; set; } = new();
|
||||
public bool Bold { get; set; }
|
||||
public bool Big { get; set; }
|
||||
public bool Tall { get; set; }
|
||||
public bool Red { get; set; }
|
||||
public int? LineSpacing { get; set; }
|
||||
}
|
||||
|
||||
public class ColumnDef
|
||||
{
|
||||
public string? Text { get; set; }
|
||||
public int? Width { get; set; }
|
||||
public string Align { get; set; } = "left";
|
||||
}
|
||||
|
||||
public class SeparatorNode : TemplateNode
|
||||
{
|
||||
public char Character { get; set; } = '-';
|
||||
}
|
||||
|
||||
public class CutNode : TemplateNode { }
|
||||
|
||||
public class FeedNode : TemplateNode
|
||||
{
|
||||
public int Lines { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class ForeachNode : TemplateNode
|
||||
{
|
||||
public string Items { get; set; } = "";
|
||||
public string Var { get; set; } = "";
|
||||
}
|
||||
|
||||
public class IfNode : TemplateNode
|
||||
{
|
||||
public string Test { get; set; } = "";
|
||||
}
|
||||
|
||||
public class ElseNode : TemplateNode { }
|
||||
|
||||
public class TableNode : TemplateNode
|
||||
{
|
||||
public List<ColumnDef> Columns { get; set; } = new();
|
||||
public string? HeaderItems { get; set; }
|
||||
public string? Items { get; set; }
|
||||
public string? Var { get; set; }
|
||||
}
|
||||
250
Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs
Normal file
250
Inspectron.Epson.TemplateEngine/Parsing/TemplateParser.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Parsing;
|
||||
|
||||
public class TemplateParser
|
||||
{
|
||||
public ReceiptNode Parse(string xml)
|
||||
{
|
||||
XDocument doc;
|
||||
try
|
||||
{
|
||||
doc = XDocument.Parse(xml);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new TemplateParsingException($"Invalid XML: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
var root = doc.Root;
|
||||
if (root == null || root.Name.LocalName != "receipt")
|
||||
throw new TemplateParsingException("Root element must be <receipt>");
|
||||
|
||||
var receiptNode = new ReceiptNode();
|
||||
ParseChildren(root, receiptNode.Children);
|
||||
return receiptNode;
|
||||
}
|
||||
|
||||
private void ParseChildren(XElement parent, List<TemplateNode> children)
|
||||
{
|
||||
foreach (var element in parent.Elements())
|
||||
{
|
||||
var node = ParseElement(element);
|
||||
if (node != null)
|
||||
children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
private TemplateNode? ParseElement(XElement element)
|
||||
{
|
||||
return element.Name.LocalName switch
|
||||
{
|
||||
"line" => ParseLine(element),
|
||||
"columns" => ParseColumns(element),
|
||||
"row" => ParseRow(element),
|
||||
"separator" => ParseSeparator(element),
|
||||
"cut" => new CutNode(),
|
||||
"feed" => ParseFeed(element),
|
||||
"foreach" => ParseForeach(element),
|
||||
"if" => ParseIf(element),
|
||||
"else" => ParseElse(element),
|
||||
"table" => ParseTable(element),
|
||||
_ => throw new TemplateParsingException($"Unknown element: <{element.Name.LocalName}>")
|
||||
};
|
||||
}
|
||||
|
||||
private LineNode ParseLine(XElement element)
|
||||
{
|
||||
var node = new LineNode();
|
||||
ApplyFormatting(element, node);
|
||||
|
||||
// Text content: either inner text (with {{}} expressions) or empty line
|
||||
var text = GetTextContent(element);
|
||||
node.Text = text;
|
||||
|
||||
node.Align = GetAttr(element, "align", "left");
|
||||
node.Wrap = GetBoolAttr(element, "wrap");
|
||||
node.WrapIndent = GetIntAttr(element, "wrapIndent", 0);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private ColumnsNode ParseColumns(XElement element)
|
||||
{
|
||||
var node = new ColumnsNode();
|
||||
ApplyFormatting(element, node);
|
||||
|
||||
node.Left = GetAttr(element, "left", "");
|
||||
node.Right = GetAttr(element, "right", "");
|
||||
node.Wrap = GetBoolAttr(element, "wrap");
|
||||
node.WrapIndent = GetIntAttr(element, "wrapIndent", 0);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private RowNode ParseRow(XElement element)
|
||||
{
|
||||
var node = new RowNode();
|
||||
ApplyFormatting(element, node);
|
||||
|
||||
foreach (var colElement in element.Elements("col"))
|
||||
{
|
||||
var col = new ColumnDef
|
||||
{
|
||||
Text = GetTextContent(colElement),
|
||||
Width = GetNullableIntAttr(colElement, "width"),
|
||||
Align = GetAttr(colElement, "align", "left")
|
||||
};
|
||||
node.Columns.Add(col);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private SeparatorNode ParseSeparator(XElement element)
|
||||
{
|
||||
var charAttr = GetAttr(element, "char", "-");
|
||||
return new SeparatorNode
|
||||
{
|
||||
Character = string.IsNullOrEmpty(charAttr) ? '-' : charAttr[0]
|
||||
};
|
||||
}
|
||||
|
||||
private FeedNode ParseFeed(XElement element)
|
||||
{
|
||||
return new FeedNode
|
||||
{
|
||||
Lines = GetIntAttr(element, "lines", 1)
|
||||
};
|
||||
}
|
||||
|
||||
private ForeachNode ParseForeach(XElement element)
|
||||
{
|
||||
var items = GetRequiredAttr(element, "items", "foreach")!;
|
||||
var var_ = GetRequiredAttr(element, "var", "foreach")!;
|
||||
|
||||
var node = new ForeachNode { Items = items, Var = var_ };
|
||||
ParseChildren(element, node.Children);
|
||||
return node;
|
||||
}
|
||||
|
||||
private IfNode ParseIf(XElement element)
|
||||
{
|
||||
var test = GetRequiredAttr(element, "test", "if")!;
|
||||
|
||||
var node = new IfNode { Test = test };
|
||||
ParseChildren(element, node.Children);
|
||||
return node;
|
||||
}
|
||||
|
||||
private ElseNode ParseElse(XElement element)
|
||||
{
|
||||
var node = new ElseNode();
|
||||
ParseChildren(element, node.Children);
|
||||
return node;
|
||||
}
|
||||
|
||||
private TableNode ParseTable(XElement element)
|
||||
{
|
||||
var node = new TableNode
|
||||
{
|
||||
Items = GetAttr(element, "items", null),
|
||||
Var = GetAttr(element, "var", null),
|
||||
HeaderItems = GetAttr(element, "headerItems", null)
|
||||
};
|
||||
|
||||
foreach (var colElement in element.Elements("col"))
|
||||
{
|
||||
var col = new ColumnDef
|
||||
{
|
||||
Text = GetTextContent(colElement),
|
||||
Width = GetNullableIntAttr(colElement, "width"),
|
||||
Align = GetAttr(colElement, "align", "left")
|
||||
};
|
||||
node.Columns.Add(col);
|
||||
}
|
||||
|
||||
// Parse non-col children (col elements are consumed above as column definitions)
|
||||
foreach (var child in element.Elements().Where(e => e.Name.LocalName != "col"))
|
||||
{
|
||||
var childNode = ParseElement(child);
|
||||
if (childNode != null)
|
||||
node.Children.Add(childNode);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static void ApplyFormatting(XElement element, LineNode node)
|
||||
{
|
||||
node.Bold = GetBoolAttr(element, "bold");
|
||||
node.Big = GetBoolAttr(element, "big");
|
||||
node.Tall = GetBoolAttr(element, "tall");
|
||||
node.Red = GetBoolAttr(element, "red");
|
||||
node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
|
||||
}
|
||||
|
||||
private static void ApplyFormatting(XElement element, ColumnsNode node)
|
||||
{
|
||||
node.Bold = GetBoolAttr(element, "bold");
|
||||
node.Big = GetBoolAttr(element, "big");
|
||||
node.Tall = GetBoolAttr(element, "tall");
|
||||
node.Red = GetBoolAttr(element, "red");
|
||||
node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
|
||||
}
|
||||
|
||||
private static void ApplyFormatting(XElement element, RowNode node)
|
||||
{
|
||||
node.Bold = GetBoolAttr(element, "bold");
|
||||
node.Big = GetBoolAttr(element, "big");
|
||||
node.Tall = GetBoolAttr(element, "tall");
|
||||
node.Red = GetBoolAttr(element, "red");
|
||||
node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
|
||||
}
|
||||
|
||||
private static string GetTextContent(XElement element)
|
||||
{
|
||||
// Get all inner text content (may include {{}} expressions)
|
||||
// Use element.Value to get concatenated text of all text nodes
|
||||
if (!element.HasElements)
|
||||
return element.Value;
|
||||
|
||||
// If element has child elements, only get direct text nodes
|
||||
return string.Concat(element.Nodes().OfType<XText>().Select(t => t.Value));
|
||||
}
|
||||
|
||||
private static string? GetRequiredAttr(XElement element, string name, string elementName)
|
||||
{
|
||||
var attr = element.Attribute(name);
|
||||
if (attr == null)
|
||||
throw new TemplateParsingException($"<{elementName}> requires '{name}' attribute");
|
||||
return attr.Value;
|
||||
}
|
||||
|
||||
private static string GetAttr(XElement element, string name, string? defaultValue)
|
||||
{
|
||||
var attr = element.Attribute(name);
|
||||
return attr?.Value ?? defaultValue ?? "";
|
||||
}
|
||||
|
||||
private static bool GetBoolAttr(XElement element, string name)
|
||||
{
|
||||
var attr = element.Attribute(name);
|
||||
if (attr == null) return false;
|
||||
return attr.Value.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static int GetIntAttr(XElement element, string name, int defaultValue)
|
||||
{
|
||||
var attr = element.Attribute(name);
|
||||
if (attr == null) return defaultValue;
|
||||
return int.TryParse(attr.Value, out var v) ? v : defaultValue;
|
||||
}
|
||||
|
||||
private static int? GetNullableIntAttr(XElement element, string name)
|
||||
{
|
||||
var attr = element.Attribute(name);
|
||||
if (attr == null) return null;
|
||||
return int.TryParse(attr.Value, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
38
Inspectron.Epson.TemplateEngine/ReceiptTemplateEngine.cs
Normal file
38
Inspectron.Epson.TemplateEngine/ReceiptTemplateEngine.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
using Inspectron.Epson.TemplateEngine.Parsing;
|
||||
using Inspectron.Epson.TemplateEngine.Rendering;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine;
|
||||
|
||||
public class ReceiptTemplateEngine
|
||||
{
|
||||
public List<PrintCommand> Render(
|
||||
string xmlTemplate,
|
||||
string jsonData,
|
||||
int lineWidth = 48,
|
||||
int bigFontLineWidth = 24)
|
||||
{
|
||||
// Parse XML template into node tree
|
||||
var parser = new TemplateParser();
|
||||
var receiptNode = parser.Parse(xmlTemplate);
|
||||
|
||||
// Parse JSON data into DataContext
|
||||
JsonElement root;
|
||||
try
|
||||
{
|
||||
root = JsonDocument.Parse(jsonData).RootElement;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new TemplateRenderingException($"Invalid JSON data: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
var context = new DataContext(root);
|
||||
|
||||
// Render node tree to PrintCommands
|
||||
var renderer = new TemplateRenderer(lineWidth, bigFontLineWidth);
|
||||
return renderer.Render(receiptNode, context);
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
642
Inspectron.Epson.TemplateEngine/template_syntax.md
Normal file
642
Inspectron.Epson.TemplateEngine/template_syntax.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# 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>` |
|
||||
Reference in New Issue
Block a user