Files
Print_server/Inspectron.Epson.Templates/Language/Parser.cs
2026-01-21 15:56:37 +01:00

378 lines
11 KiB
C#

using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Language;
public class Parser
{
private readonly List<Token> _tokens;
private int _position;
private readonly List<ParseError> _errors = new();
public Parser(List<Token> 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<ITemplateNode> ParseNodeList(params TokenType[] terminators)
{
var nodes = new List<ITemplateNode>();
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<ITemplateNode>();
// 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<ConditionBranch>();
List<ITemplateNode>? 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;
Advance(); // consume @row
SkipNewlines();
var columns = new List<ColumnNode>();
// 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<ITemplateNode>();
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, rowToken.Position);
}
private ColumnNode ParseColumnDef()
{
var columnToken = Current;
var (width, alignment) = ColumnNode.ParseColumnSpec(columnToken.Value);
Advance(); // consume column token
var content = new List<ITemplateNode>();
// 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<ITemplateNode>();
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<ParseError> Errors)
{
public bool HasErrors => Errors.Count > 0;
}
public record ParseError(string Message, SourcePosition Position);