templates support

This commit is contained in:
EugeneTes
2026-01-21 15:56:37 +01:00
parent 729679d283
commit 9fbc61dede
60 changed files with 5678 additions and 14 deletions

View File

@@ -0,0 +1,219 @@
using Inspectron.Epson.Templates.Language;
namespace Inspectron.Epson.Templates.Tests.Language;
public class CommentTests
{
#region Single-line comments
[Fact]
public void Tokenize_SingleLineComment_ProducesNoTextTokens()
{
var lexer = new Lexer("@* This is a comment");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text);
}
[Fact]
public void Tokenize_InlineSingleLineComment_ProducesNoTextTokens()
{
var lexer = new Lexer("@* This is a comment *@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text);
}
[Fact]
public void Tokenize_IndentedSingleLineComment_IsRecognizedAsComment()
{
var lexer = new Lexer(" @* indented comment");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text);
}
[Fact]
public void Tokenize_TextBeforeAndAfterCommentLine_IsPreserved()
{
var lexer = new Lexer("Before\n@* comment\nAfter");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Before");
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "After");
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("comment"));
}
[Fact]
public void Tokenize_InlineCommentWithContentAfter_PreservesContentAfter()
{
var lexer = new Lexer("@* comment *@After");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "After");
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("comment"));
}
#endregion
#region Multi-line comments
[Fact]
public void Tokenize_MultiLineComment_ProducesNoTextTokens()
{
var lexer = new Lexer("@*\nThis is multi-line\ncomment text\n*@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text);
}
[Fact]
public void Tokenize_MultiLineComment_ContentBeforeAndAfterPreserved()
{
var lexer = new Lexer("Before\n@*\ncomment\n*@\nAfter");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Before");
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "After");
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("comment"));
}
[Fact]
public void Tokenize_UnclosedMultiLineComment_ProducesError()
{
var lexer = new Lexer("@*\nThis comment is never closed");
var result = lexer.Tokenize();
Assert.True(result.HasErrors);
Assert.Contains(result.Errors, e => e.Message.Contains("Unclosed multi-line comment"));
}
[Fact]
public void Tokenize_MultiLineCommentWithContentAfterCloser_PreservesContent()
{
var lexer = new Lexer("@*\ncomment\n*@After");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "After");
}
#endregion
#region Edge cases
[Fact]
public void Tokenize_StarSeparator_StillWorksNotConfusedWithComment()
{
var lexer = new Lexer("***");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.StarSeparator);
}
[Fact]
public void Tokenize_DirectivesInsideComment_AreIgnored()
{
var lexer = new Lexer("@*\n@if condition\n@foreach item in items\n*@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.If);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Foreach);
}
[Fact]
public void Tokenize_BindingsInsideComment_AreIgnored()
{
var lexer = new Lexer("@* {PropertyName} *@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Binding);
}
[Fact]
public void Tokenize_CommentWithMultipleStars_HandledCorrectly()
{
var lexer = new Lexer("@*** This has extra stars ***@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("stars"));
}
[Fact]
public void Tokenize_EmptyMultiLineComment_HandledCorrectly()
{
var lexer = new Lexer("@*\n*@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text);
}
[Fact]
public void Tokenize_CommentInComplexTemplate_WorksCorrectly()
{
var template = @"#bold,center# Title #
@* This is a header comment *@
---
@if HasItems
@* Loop through items *@
@foreach item in Items
{item.Name}
@end
@end";
var lexer = new Lexer(template);
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.StyleStart);
Assert.Contains(result.Tokens, t => t.Type == TokenType.DashSeparator);
Assert.Contains(result.Tokens, t => t.Type == TokenType.If);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Foreach);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("comment"));
}
[Fact]
public void Tokenize_MultipleCommentsInTemplate_AllStripped()
{
var template = @"Line 1
@* Comment 1 *@
Line 2
@* Comment 2
still comment
*@
Line 3";
var lexer = new Lexer(template);
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Line 1");
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Line 2");
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Line 3");
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.Text && t.Value.Contains("Comment"));
}
[Fact]
public void Tokenize_SeparatorInsideMultiLineComment_IsIgnored()
{
var lexer = new Lexer("@*\n---\n===\n***\n*@");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.DashSeparator);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.EqualsSeparator);
Assert.DoesNotContain(result.Tokens, t => t.Type == TokenType.StarSeparator);
}
#endregion
}

View File

@@ -0,0 +1,171 @@
using Inspectron.Epson.Templates.Language;
namespace Inspectron.Epson.Templates.Tests.Language;
public class LexerTests
{
[Fact]
public void Tokenize_PlainText_ReturnsTextToken()
{
var lexer = new Lexer("Hello World");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == "Hello World");
}
[Fact]
public void Tokenize_Binding_ReturnsBindingToken()
{
var lexer = new Lexer("{PropertyName}");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Binding && t.Value == "PropertyName");
}
[Fact]
public void Tokenize_BindingWithFormat_ReturnsBindingTokenWithFormat()
{
var lexer = new Lexer("{TransactionDateTime:dd-MMM-yy}");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Binding && t.Value == "TransactionDateTime:dd-MMM-yy");
}
[Fact]
public void Tokenize_StyleBlock_ReturnsStyleTokens()
{
var lexer = new Lexer("#bold,center# text #");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.StyleStart && t.Value == "bold,center");
Assert.Contains(result.Tokens, t => t.Type == TokenType.Text && t.Value == " text ");
Assert.Contains(result.Tokens, t => t.Type == TokenType.StyleEnd);
}
[Fact]
public void Tokenize_DashSeparator_ReturnsSeparatorToken()
{
var lexer = new Lexer("---");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.DashSeparator);
}
[Fact]
public void Tokenize_EqualsSeparator_ReturnsSeparatorToken()
{
var lexer = new Lexer("===");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.EqualsSeparator);
}
[Fact]
public void Tokenize_StarSeparator_ReturnsSeparatorToken()
{
var lexer = new Lexer("***");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.StarSeparator);
}
[Fact]
public void Tokenize_IfDirective_ReturnsIfToken()
{
var lexer = new Lexer("@if condition");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.If && t.Value == "condition");
}
[Fact]
public void Tokenize_ForeachDirective_ReturnsForeachToken()
{
var lexer = new Lexer("@foreach item in items");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Foreach && t.Value == "item in items");
}
[Fact]
public void Tokenize_EndDirective_ReturnsEndToken()
{
var lexer = new Lexer("@end");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.End);
}
[Fact]
public void Tokenize_RowDirectives_ReturnsRowTokens()
{
var lexer = new Lexer("@row\n@endrow");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Row);
Assert.Contains(result.Tokens, t => t.Type == TokenType.EndRow);
}
[Fact]
public void Tokenize_Column_ReturnsColumnToken()
{
var lexer = new Lexer("|20|");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Column && t.Value.StartsWith("20"));
}
[Fact]
public void Tokenize_ColumnWithAlignment_ReturnsColumnTokenWithAlignment()
{
var lexer = new Lexer("|20,right|");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Column && t.Value == "20,right");
}
[Fact]
public void Tokenize_MultipleLines_PreservesLineStructure()
{
var lexer = new Lexer("Line 1\nLine 2\nLine 3");
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
var newlines = result.Tokens.Count(t => t.Type == TokenType.NewLine);
Assert.Equal(3, newlines);
}
[Fact]
public void Tokenize_ComplexTemplate_ParsesCorrectly()
{
var template = @"#bold,center# {Title} #
---
@if HasItems
@foreach item in Items
{item.Name}
@end
@end";
var lexer = new Lexer(template);
var result = lexer.Tokenize();
Assert.False(result.HasErrors);
Assert.Contains(result.Tokens, t => t.Type == TokenType.StyleStart);
Assert.Contains(result.Tokens, t => t.Type == TokenType.DashSeparator);
Assert.Contains(result.Tokens, t => t.Type == TokenType.If);
Assert.Contains(result.Tokens, t => t.Type == TokenType.Foreach);
Assert.Equal(2, result.Tokens.Count(t => t.Type == TokenType.End));
}
}

View File

@@ -0,0 +1,155 @@
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Language;
public class ParserTests
{
private ParseResult Parse(string source)
{
var lexer = new Lexer(source);
var lexerResult = lexer.Tokenize();
var parser = new Parser(lexerResult.Tokens);
return parser.Parse();
}
[Fact]
public void Parse_PlainText_ReturnsTextNode()
{
var result = Parse("Hello World");
Assert.False(result.HasErrors);
Assert.Single(result.Template.ChildNodes.OfType<TextNode>());
}
[Fact]
public void Parse_Binding_ReturnsBindingNode()
{
var result = Parse("{Name}");
Assert.False(result.HasErrors);
var binding = Assert.Single(result.Template.ChildNodes.OfType<BindingNode>());
Assert.Equal("Name", binding.Path);
Assert.Null(binding.Format);
}
[Fact]
public void Parse_BindingWithFormat_PreservesFormat()
{
var result = Parse("{Date:yyyy-MM-dd}");
Assert.False(result.HasErrors);
var binding = Assert.Single(result.Template.ChildNodes.OfType<BindingNode>());
Assert.Equal("Date", binding.Path);
Assert.Equal("yyyy-MM-dd", binding.Format);
}
[Fact]
public void Parse_StyledText_ReturnsStyledTextNode()
{
var result = Parse("#bold,center# text #");
Assert.False(result.HasErrors);
var styled = Assert.Single(result.Template.ChildNodes.OfType<StyledTextNode>());
Assert.Contains("bold", styled.Styles);
Assert.Contains("center", styled.Styles);
}
[Fact]
public void Parse_DashSeparator_ReturnsSeparatorNode()
{
var result = Parse("---");
Assert.False(result.HasErrors);
var separator = Assert.Single(result.Template.ChildNodes.OfType<SeparatorNode>());
Assert.Equal(SeparatorStyle.Dash, separator.Style);
}
[Fact]
public void Parse_IfBlock_ReturnsIfNode()
{
var result = Parse("@if condition\ntext\n@end");
Assert.False(result.HasErrors);
var ifNode = Assert.Single(result.Template.ChildNodes.OfType<IfNode>());
Assert.Equal("condition", ifNode.IfBranch.Condition);
Assert.NotEmpty(ifNode.IfBranch.Body);
}
[Fact]
public void Parse_IfElseBlock_ReturnsIfNodeWithElseBranch()
{
var result = Parse("@if condition\nif-text\n@else\nelse-text\n@end");
Assert.False(result.HasErrors);
var ifNode = Assert.Single(result.Template.ChildNodes.OfType<IfNode>());
Assert.NotNull(ifNode.ElseBranch);
}
[Fact]
public void Parse_IfElseIfElseBlock_ReturnsAllBranches()
{
var result = Parse("@if cond1\ntext1\n@elseif cond2\ntext2\n@else\ntext3\n@end");
Assert.False(result.HasErrors);
var ifNode = Assert.Single(result.Template.ChildNodes.OfType<IfNode>());
Assert.Single(ifNode.ElseIfBranches);
Assert.NotNull(ifNode.ElseBranch);
}
[Fact]
public void Parse_ForeachBlock_ReturnsForeachNode()
{
var result = Parse("@foreach item in items\n{item}\n@end");
Assert.False(result.HasErrors);
var foreach_ = Assert.Single(result.Template.ChildNodes.OfType<ForeachNode>());
Assert.Equal("item", foreach_.ItemVariable);
Assert.Equal("items", foreach_.CollectionPath);
}
[Fact]
public void Parse_NestedBlocks_HandlesNestingCorrectly()
{
var result = Parse("@foreach item in items\n@if item.visible\n{item.name}\n@end\n@end");
Assert.False(result.HasErrors);
var foreach_ = Assert.Single(result.Template.ChildNodes.OfType<ForeachNode>());
Assert.Single(foreach_.Body.OfType<IfNode>());
}
[Fact]
public void Parse_RowWithColumns_ReturnsRowNode()
{
var result = Parse("@row\n|20|Name|10,right|Value\n@endrow");
Assert.False(result.HasErrors);
var row = Assert.Single(result.Template.ChildNodes.OfType<RowNode>());
Assert.Equal(2, row.Columns.Count);
}
[Fact]
public void Parse_EmptyLine_ReturnsEmptyLineNode()
{
var result = Parse("line1\n\nline2");
Assert.False(result.HasErrors);
Assert.Contains(result.Template.ChildNodes, n => n is EmptyLineNode);
}
[Fact]
public void Parse_UnclosedIfBlock_ReportsError()
{
var result = Parse("@if condition\ntext");
Assert.True(result.HasErrors);
}
[Fact]
public void Parse_UnclosedForeachBlock_ReportsError()
{
var result = Parse("@foreach item in items\ntext");
Assert.True(result.HasErrors);
}
}