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 "); var receiptNode = new ReceiptNode(); ParseChildren(root, receiptNode.Children); return receiptNode; } private void ParseChildren(XElement parent, List 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().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; } }