using Inspectron.Epson.Templates.Language.Nodes; namespace Inspectron.Epson.Templates.Language; public class Parser { private readonly List _tokens; private int _position; private readonly List _errors = new(); public Parser(List tokens) { _tokens = tokens ?? throw new ArgumentNullException(nameof(tokens)); } public ParseResult Parse() { _position = 0; _errors.Clear(); var nodes = ParseNodeList(TokenType.EndOfFile); return new ParseResult(new TemplateNode(nodes), _errors.ToList()); } private List ParseNodeList(params TokenType[] terminators) { var nodes = new List(); while (!IsAtEnd() && !terminators.Contains(Current.Type)) { var node = ParseNode(); if (node != null) { nodes.Add(node); } } return nodes; } private ITemplateNode? ParseNode() { var token = Current; switch (token.Type) { case TokenType.NewLine: Advance(); // Check if this was an empty line (previous was also newline or start) if (_position >= 2 && _tokens[_position - 2].Type == TokenType.NewLine) { return new EmptyLineNode(token.Position); } return null; case TokenType.Text: return ParseText(); case TokenType.StyleStart: return ParseStyledText(); case TokenType.Binding: return ParseBinding(); case TokenType.DashSeparator: Advance(); return new SeparatorNode(SeparatorStyle.Dash, token.Position); case TokenType.EqualsSeparator: Advance(); return new SeparatorNode(SeparatorStyle.Equals, token.Position); case TokenType.StarSeparator: Advance(); return new SeparatorNode(SeparatorStyle.Star, token.Position); case TokenType.TildeSeparator: Advance(); return new SeparatorNode(SeparatorStyle.Tilde, token.Position); case TokenType.If: return ParseIf(); case TokenType.Foreach: return ParseForeach(); case TokenType.Row: return ParseRow(); case TokenType.Column: return ParseColumn(); case TokenType.ElseIf: case TokenType.Else: case TokenType.End: case TokenType.EndRow: // These are handled by parent parsers; don't consume here return null; case TokenType.StyleEnd: // Orphan style end - skip with error AddError("Unexpected style end marker '#' without matching style start", token.Position); Advance(); return null; case TokenType.EndOfFile: return null; default: AddError($"Unexpected token type: {token.Type}", token.Position); Advance(); return null; } } private TextNode ParseText() { var token = Current; Advance(); return new TextNode(token.Value, token.Position); } private BindingNode ParseBinding() { var token = Current; Advance(); return BindingNode.Parse(token.Value, token.Position); } private StyledTextNode ParseStyledText() { var startToken = Current; var styles = startToken.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); Advance(); // consume StyleStart var contentNodes = new List(); // Parse content until StyleEnd or NewLine while (!IsAtEnd() && Current.Type != TokenType.StyleEnd && Current.Type != TokenType.NewLine) { if (Current.Type == TokenType.Text) { contentNodes.Add(ParseText()); } else if (Current.Type == TokenType.Binding) { contentNodes.Add(ParseBinding()); } else { // Unexpected token inside styled text AddError($"Unexpected token inside styled text: {Current.Type}", Current.Position); Advance(); } } // Consume StyleEnd if present if (Current.Type == TokenType.StyleEnd) { Advance(); } return new StyledTextNode(styles, contentNodes, startToken.Position); } private IfNode ParseIf() { var ifToken = Current; var ifCondition = ifToken.Value; Advance(); // consume @if // Skip newline after @if SkipNewlines(); var ifBody = ParseNodeList(TokenType.ElseIf, TokenType.Else, TokenType.End, TokenType.EndOfFile); var elseIfBranches = new List(); List? elseBranch = null; // Parse @elseif branches while (Current.Type == TokenType.ElseIf) { var elseIfToken = Current; var elseIfCondition = elseIfToken.Value; Advance(); // consume @elseif SkipNewlines(); var elseIfBody = ParseNodeList(TokenType.ElseIf, TokenType.Else, TokenType.End, TokenType.EndOfFile); elseIfBranches.Add(new ConditionBranch(elseIfCondition, elseIfBody, elseIfToken.Position)); } // Parse @else branch if (Current.Type == TokenType.Else) { Advance(); // consume @else SkipNewlines(); elseBranch = ParseNodeList(TokenType.End, TokenType.EndOfFile); } // Expect @end if (Current.Type == TokenType.End) { Advance(); // consume @end } else { AddError("Expected @end to close @if block", Current.Position); } return new IfNode( new ConditionBranch(ifCondition, ifBody, ifToken.Position), elseIfBranches, elseBranch, ifToken.Position); } private ForeachNode ParseForeach() { var foreachToken = Current; Advance(); // consume @foreach string itemVariable; string collectionPath; try { (itemVariable, collectionPath) = ForeachNode.ParseForeach(foreachToken.Value); } catch (FormatException ex) { AddError(ex.Message, foreachToken.Position); itemVariable = "item"; collectionPath = "items"; } SkipNewlines(); var body = ParseNodeList(TokenType.End, TokenType.EndOfFile); if (Current.Type == TokenType.End) { Advance(); // consume @end } else { AddError("Expected @end to close @foreach block", Current.Position); } return new ForeachNode(itemVariable, collectionPath, body, foreachToken.Position); } private RowNode ParseRow() { var rowToken = Current; var styles = string.IsNullOrWhiteSpace(rowToken.Value) ? Array.Empty() : rowToken.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); Advance(); // consume @row SkipNewlines(); var columns = new List(); // Parse columns until @endrow while (!IsAtEnd() && Current.Type != TokenType.EndRow && Current.Type != TokenType.EndOfFile) { if (Current.Type == TokenType.Column) { columns.Add(ParseColumnDef()); } else if (Current.Type == TokenType.NewLine) { Advance(); } else { // Content outside of column definition - create implicit column var implicitContent = new List(); while (!IsAtEnd() && Current.Type != TokenType.Column && Current.Type != TokenType.EndRow && Current.Type != TokenType.NewLine && Current.Type != TokenType.EndOfFile) { var node = ParseNode(); if (node != null) implicitContent.Add(node); } if (implicitContent.Count > 0) { columns.Add(new ColumnNode(0, ColumnAlignment.Left, implicitContent, Current.Position)); } } } if (Current.Type == TokenType.EndRow) { Advance(); // consume @endrow } else { AddError("Expected @endrow to close @row block", Current.Position); } return new RowNode(columns, styles, rowToken.Position); } private ColumnNode ParseColumnDef() { var columnToken = Current; var (width, alignment) = ColumnNode.ParseColumnSpec(columnToken.Value); Advance(); // consume column token var content = new List(); // Parse content until next column, newline, or endrow while (!IsAtEnd() && Current.Type != TokenType.Column && Current.Type != TokenType.EndRow && Current.Type != TokenType.NewLine && Current.Type != TokenType.EndOfFile) { var node = ParseNode(); if (node != null) content.Add(node); } return new ColumnNode(width, alignment, content, columnToken.Position); } private ColumnNode? ParseColumn() { // Standalone column outside of @row var columnToken = Current; var (width, alignment) = ColumnNode.ParseColumnSpec(columnToken.Value); Advance(); var content = new List(); while (!IsAtEnd() && Current.Type != TokenType.Column && Current.Type != TokenType.NewLine && Current.Type != TokenType.EndOfFile) { var node = ParseNode(); if (node != null) content.Add(node); } return new ColumnNode(width, alignment, content, columnToken.Position); } private void SkipNewlines() { while (Current.Type == TokenType.NewLine) { Advance(); } } private Token Current => _position < _tokens.Count ? _tokens[_position] : Token.Eof(_tokens.Count); private bool IsAtEnd() => _position >= _tokens.Count || Current.Type == TokenType.EndOfFile; private void Advance() { if (!IsAtEnd()) _position++; } private void AddError(string message, SourcePosition position) { _errors.Add(new ParseError(message, position)); } } public record ParseResult(TemplateNode Template, List Errors) { public bool HasErrors => Errors.Count > 0; } public record ParseError(string Message, SourcePosition Position);