379 lines
11 KiB
C#
379 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();
|
|
if(token.Position.Column==1)
|
|
return new NewLineNode(token.Position);
|
|
else
|
|
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;
|
|
var styles = string.IsNullOrWhiteSpace(rowToken.Value)
|
|
? Array.Empty<string>()
|
|
: rowToken.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
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, styles, 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);
|