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,95 @@
using Inspectron.Epson.Templates.Configuration;
namespace Inspectron.Epson.Templates.Tests.Configuration;
public class PrinterProfileTests
{
[Fact]
public void Default_Profile_HasExpectedValues()
{
var profile = PrinterProfile.Default;
Assert.Equal("default", profile.Id);
Assert.Equal(48, profile.LineWidth);
Assert.Equal(24, profile.BigLineWidth);
Assert.False(profile.SupportsRed);
}
}
public class PrinterProfileRegistryTests
{
[Fact]
public void GetProfile_TmT30III_ReturnsCorrectProfile()
{
var registry = new PrinterProfileRegistry();
var profile = registry.GetProfile(0x01);
Assert.Equal("tm-t30iii", profile.Id);
Assert.Equal(48, profile.LineWidth);
Assert.Equal(24, profile.BigLineWidth);
Assert.False(profile.SupportsRed);
}
[Fact]
public void GetProfile_TmU220II_0x0D_ReturnsCorrectProfile()
{
var registry = new PrinterProfileRegistry();
var profile = registry.GetProfile(0x0D);
Assert.Equal("tm-u220ii", profile.Id);
Assert.Equal(33, profile.LineWidth);
Assert.Equal(20, profile.BigLineWidth);
Assert.True(profile.SupportsRed);
}
[Fact]
public void GetProfile_TmU220II_0x13_ReturnsCorrectProfile()
{
var registry = new PrinterProfileRegistry();
var profile = registry.GetProfile(0x13);
Assert.Equal("tm-u220ii", profile.Id);
Assert.Equal(33, profile.LineWidth);
Assert.Equal(20, profile.BigLineWidth);
Assert.True(profile.SupportsRed);
}
[Fact]
public void GetProfile_UnknownPrinter_ReturnsDefault()
{
var registry = new PrinterProfileRegistry();
var profile = registry.GetProfile(0xFF);
Assert.Equal(PrinterProfile.Default.LineWidth, profile.LineWidth);
}
[Fact]
public void GetProfile_ByName_ReturnsCorrectProfile()
{
var registry = new PrinterProfileRegistry();
var profile = registry.GetProfile("tm-t30iii");
Assert.Equal("tm-t30iii", profile.Id);
}
[Fact]
public void Register_CustomProfile_CanBeRetrieved()
{
var registry = new PrinterProfileRegistry();
var customProfile = new PrinterProfile("custom", "Custom Printer", 40, 20, true);
registry.Register(0xAA, customProfile);
var retrieved = registry.GetProfile(0xAA);
Assert.Equal("custom", retrieved.Id);
}
[Fact]
public void GetAllProfiles_ReturnsDistinctProfiles()
{
var registry = new PrinterProfileRegistry();
var profiles = registry.GetAllProfiles().ToList();
// Should have TM-T30III and TM-U220II (distinct)
Assert.Equal(2, profiles.Count);
}
}

View File

@@ -0,0 +1,100 @@
using Inspectron.Epson.Templates.Configuration;
namespace Inspectron.Epson.Templates.Tests.Configuration;
public class TemplateResolverTests
{
[Fact]
public void Resolve_ExactMatch_ReturnsExactTemplate()
{
var assignments = new[]
{
new TemplateAssignment(1, "tm-t30iii", "kitchen-t30.template"),
new TemplateAssignment(1, null, "kitchen-default.template")
};
var resolver = new TemplateResolver(assignments);
var result = resolver.Resolve(1, "tm-t30iii");
Assert.Equal("kitchen-t30.template", result);
}
[Fact]
public void Resolve_TypeOnlyMatch_ReturnsTypeTemplate()
{
var assignments = new[]
{
new TemplateAssignment(1, null, "kitchen-default.template"),
new TemplateAssignment(2, null, "bar-default.template")
};
var resolver = new TemplateResolver(assignments);
var result = resolver.Resolve(1, "unknown-profile");
Assert.Equal("kitchen-default.template", result);
}
[Fact]
public void Resolve_NoMatch_ReturnsFallback()
{
var assignments = new[]
{
new TemplateAssignment(1, null, "kitchen-default.template")
};
var resolver = new TemplateResolver(assignments, "fallback.template");
var result = resolver.Resolve(99, "unknown-profile");
Assert.Equal("fallback.template", result);
}
[Fact]
public void Resolve_PreferExactOverTypeOnly()
{
var assignments = new[]
{
new TemplateAssignment(1, null, "type-only.template"),
new TemplateAssignment(1, "tm-t30iii", "exact.template")
};
var resolver = new TemplateResolver(assignments);
var result = resolver.Resolve(1, "tm-t30iii");
Assert.Equal("exact.template", result);
}
[Fact]
public void Resolve_WithRegistry_MapsProfileCorrectly()
{
var registry = new PrinterProfileRegistry();
var assignments = new[]
{
new TemplateAssignment(1, "tm-t30iii", "kitchen-t30.template"),
new TemplateAssignment(1, "tm-u220ii", "kitchen-u220.template")
};
var resolver = new TemplateResolver(assignments);
var result = resolver.Resolve(1, 0x01, registry);
Assert.Equal("kitchen-t30.template", result);
}
[Fact]
public void LoadConfiguration_ParsesCorrectly()
{
var json = """
{
"assignments": [
{"receiptType": 1, "profileId": "tm-t30iii", "template": "kitchen.template"},
{"receiptType": 2, "profileId": null, "template": "bar.template"}
],
"fallbackTemplate": "default.template"
}
""";
var config = TemplateConfiguration.LoadFromJson(json);
Assert.Equal(2, config.Assignments.Count);
Assert.Equal("default.template", config.FallbackTemplate);
}
}

View File

@@ -0,0 +1,83 @@
using Inspectron.Epson.PrintServer.Printers.Utils;
namespace Inspectron.Epson.Templates.Tests.Helpers;
public static class PrintCommandAssertions
{
public static void AssertCommandExists(
List<PrintCommand> commands,
string textContains,
bool? isBold = null,
bool? isBig = null,
bool? isTall = null,
bool? isRed = null)
{
var matching = commands.Where(c => c.Text.Contains(textContains)).ToList();
if (matching.Count == 0)
{
throw new Xunit.Sdk.XunitException($"No command found containing text: '{textContains}'");
}
foreach (var cmd in matching)
{
if (isBold.HasValue && cmd.IsBold != isBold.Value)
{
throw new Xunit.Sdk.XunitException(
$"Command with text '{textContains}' has IsBold={cmd.IsBold}, expected {isBold.Value}");
}
if (isBig.HasValue && cmd.IsBig != isBig.Value)
{
throw new Xunit.Sdk.XunitException(
$"Command with text '{textContains}' has IsBig={cmd.IsBig}, expected {isBig.Value}");
}
if (isTall.HasValue && cmd.IsTall != isTall.Value)
{
throw new Xunit.Sdk.XunitException(
$"Command with text '{textContains}' has IsTall={cmd.IsTall}, expected {isTall.Value}");
}
if (isRed.HasValue && cmd.IsRed != isRed.Value)
{
throw new Xunit.Sdk.XunitException(
$"Command with text '{textContains}' has IsRed={cmd.IsRed}, expected {isRed.Value}");
}
}
}
public static void AssertSequence(List<PrintCommand> commands, params string[] expectedTexts)
{
var commandTexts = commands.Select(c => c.Text).ToList();
int searchIndex = 0;
foreach (var expected in expectedTexts)
{
var foundIndex = -1;
for (int i = searchIndex; i < commandTexts.Count; i++)
{
if (commandTexts[i].Contains(expected))
{
foundIndex = i;
break;
}
}
if (foundIndex < 0)
{
throw new Xunit.Sdk.XunitException(
$"Expected text '{expected}' not found in sequence after index {searchIndex}");
}
searchIndex = foundIndex + 1;
}
}
public static void AssertCommandCount(List<PrintCommand> commands, string textContains, int expectedCount)
{
var count = commands.Count(c => c.Text.Contains(textContains));
if (count != expectedCount)
{
throw new Xunit.Sdk.XunitException(
$"Expected {expectedCount} commands containing '{textContains}', found {count}");
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Inspectron.Epson.Templates\Inspectron.Epson.Templates.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,193 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
using Inspectron.Epson.Templates.Tests.Helpers;
namespace Inspectron.Epson.Templates.Tests.Integration;
public class KitchenReceiptTests
{
private readonly PrinterProfile _profile = new("tm-t30iii", "TM-T30III", 48, 24, false);
private const string KitchenReceiptJson = """
{
"Title": "Restaurant Test",
"TransactionDateTime": "2024-03-15T14:30:00",
"ReceiptNumber": "12345",
"WaiterName": "John Doe",
"WaiterId": "W001",
"TableNumber": "5",
"SpecialInstruction": "Rush Order",
"Gangs": [
{
"Id": 1,
"Name": "Starters",
"Dishes": [
{
"Number": 2,
"Name": "Caesar Salad",
"Modifications": {
"Removed": ["Croutons"],
"Added": ["Extra Dressing"]
},
"Comment": "No anchovies"
}
]
}
],
"Dishes": [
{
"Number": 1,
"Name": "Grilled Salmon",
"Modifications": {
"Removed": [],
"Added": []
},
"Comment": null
}
]
}
""";
private const string KitchenTemplate = """
#red,big,tall,center# {Title} #
---
#center# {TransactionDateTime:dd-MMM-yy HH:mm} Nr.:{ReceiptNumber} #
#center# {WaiterName} #
#center# {WaiterId} #
#big,bold,center# Tisch: {TableNumber} #
@if SpecialInstruction
#big,bold,center# {SpecialInstruction} #
@end
---
@foreach gang in Gangs
#red,big,tall,center# {gang.Id}. {gang.Name} #
@foreach dish in gang.Dishes
#tall# {dish.Number}x {dish.Name} #
@if dish.Modifications.Removed.count > 0
@foreach removed in dish.Modifications.Removed
#bold,tall# - {removed} #
@end
@end
@if dish.Modifications.Added.count > 0
@foreach added in dish.Modifications.Added
#bold,tall# + {added} #
@end
@end
@if dish.Comment
#bold,tall# Comment: {dish.Comment} #
@end
@end
@end
@if Gangs.count > 0
---
@end
@foreach dish in Dishes
#tall# {dish.Number}x {dish.Name} #
@end
""";
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void KitchenReceipt_RendersFully()
{
var template = Parse(KitchenTemplate);
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, KitchenReceiptJson);
// Verify key elements are present
Assert.Contains(commands, c => c.Text.Contains("Restaurant Test"));
Assert.Contains(commands, c => c.Text.Contains("John Doe"));
Assert.Contains(commands, c => c.Text.Contains("Tisch: 5"));
Assert.Contains(commands, c => c.Text.Contains("Rush Order"));
Assert.Contains(commands, c => c.Text.Contains("1. Starters"));
Assert.Contains(commands, c => c.Text.Contains("2x Caesar Salad"));
Assert.Contains(commands, c => c.Text.Contains("- Croutons"));
Assert.Contains(commands, c => c.Text.Contains("+ Extra Dressing"));
Assert.Contains(commands, c => c.Text.Contains("Comment: No anchovies"));
Assert.Contains(commands, c => c.Text.Contains("1x Grilled Salmon"));
}
[Fact]
public void KitchenReceipt_AppliesStyles()
{
var template = Parse(KitchenTemplate);
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, KitchenReceiptJson);
// Title should be big and tall
var titleCmd = commands.FirstOrDefault(c => c.Text.Contains("Restaurant Test"));
Assert.NotNull(titleCmd);
Assert.True(titleCmd.IsBig);
Assert.True(titleCmd.IsTall);
// Table should be bold and big
var tableCmd = commands.FirstOrDefault(c => c.Text.Contains("Tisch: 5"));
Assert.NotNull(tableCmd);
Assert.True(tableCmd.IsBold);
Assert.True(tableCmd.IsBig);
// Modifications should be bold and tall
var modCmd = commands.FirstOrDefault(c => c.Text.Contains("- Croutons"));
Assert.NotNull(modCmd);
Assert.True(modCmd.IsBold);
Assert.True(modCmd.IsTall);
}
[Fact]
public void KitchenReceipt_GeneratesSeparators()
{
var template = Parse(KitchenTemplate);
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, KitchenReceiptJson);
var separatorCount = commands.Count(c => c.Text.All(ch => ch == '-') && c.Text.Length == 48);
Assert.True(separatorCount >= 2, $"Expected at least 2 separators, found {separatorCount}");
}
[Fact]
public void KitchenReceipt_FormatsDateTime()
{
var template = Parse(KitchenTemplate);
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, KitchenReceiptJson);
Assert.Contains(commands, c => c.Text.Contains("15-Mar-24") || c.Text.Contains("15-Mär-24"));
}
[Fact]
public void KitchenReceipt_WithoutSpecialInstruction_OmitsSection()
{
var jsonWithoutInstruction = """
{
"Title": "Test",
"TransactionDateTime": "2024-03-15T14:30:00",
"ReceiptNumber": "123",
"WaiterName": "John",
"WaiterId": "W001",
"TableNumber": "1",
"SpecialInstruction": null,
"Gangs": [],
"Dishes": []
}
""";
var template = Parse(KitchenTemplate);
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, jsonWithoutInstruction);
// Should not have any content that looks like special instruction
Assert.DoesNotContain(commands, c => c.Text.Contains("Rush"));
}
}

View File

@@ -0,0 +1,132 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Interpreter;
public class InterpreterBindingTests
{
private readonly PrinterProfile _profile = new("test", "Test Printer", 48, 24, false);
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void Interpret_SimpleBinding_ResolvesValue()
{
var template = Parse("{Name}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Name": "John"}""");
Assert.Contains(commands, c => c.Text == "John");
}
[Fact]
public void Interpret_NestedBinding_ResolvesNestedValue()
{
var template = Parse("{Person.Name}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Person": {"Name": "Jane"}}""");
Assert.Contains(commands, c => c.Text == "Jane");
}
[Fact]
public void Interpret_NumericBinding_OutputsNumber()
{
var template = Parse("{Amount}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Amount": 42}""");
Assert.Contains(commands, c => c.Text == "42");
}
[Fact]
public void Interpret_NumericBinding_WithFormat_FormatsNumber()
{
var template = Parse("{Price:F2}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Price": 19.5}""");
Assert.Contains(commands, c => c.Text == "19.50");
}
[Fact]
public void Interpret_DateBinding_WithFormat_FormatsDate()
{
var template = Parse("{Date:dd-MMM-yy}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Date": "2024-03-15T14:30:00"}""");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("Mar"));
Assert.NotNull(cmd);
Assert.Contains("15", cmd.Text);
}
[Fact]
public void Interpret_MissingBinding_OutputsEmptyString()
{
var template = Parse("{Missing}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Other": "value"}""");
Assert.Contains(commands, c => c.Text == "");
}
[Fact]
public void Interpret_NullBinding_OutputsEmptyString()
{
var template = Parse("{Value}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Value": null}""");
Assert.Contains(commands, c => c.Text == "");
}
[Fact]
public void Interpret_ArrayCountBinding_OutputsCount()
{
var template = Parse("{Items.count}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": [1, 2, 3, 4, 5]}""");
Assert.Contains(commands, c => c.Text == "5");
}
[Fact]
public void Interpret_BoolBinding_OutputsTrue()
{
var template = Parse("{Flag}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Flag": true}""");
Assert.Contains(commands, c => c.Text == "true");
}
[Fact]
public void Interpret_BoolBinding_OutputsFalse()
{
var template = Parse("{Flag}");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Flag": false}""");
Assert.Contains(commands, c => c.Text == "false");
}
[Fact]
public void Interpret_BindingInStyledText_AppliesStyleToResolvedValue()
{
var template = Parse("#bold# {Name} #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Name": "Test"}""");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("Test"));
Assert.NotNull(cmd);
Assert.True(cmd.IsBold);
}
}

View File

@@ -0,0 +1,161 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Interpreter;
public class InterpreterConditionTests
{
private readonly PrinterProfile _profile = new("test", "Test Printer", 48, 24, false);
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void Interpret_IfTruthy_ExecutesBody()
{
var template = Parse("@if HasValue\nYes\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"HasValue": true}""");
Assert.Contains(commands, c => c.Text == "Yes");
}
[Fact]
public void Interpret_IfFalsy_SkipsBody()
{
var template = Parse("@if HasValue\nYes\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"HasValue": false}""");
Assert.DoesNotContain(commands, c => c.Text == "Yes");
}
[Fact]
public void Interpret_IfStringEmpty_IsFalsy()
{
var template = Parse("@if Name\nHas Name\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Name": ""}""");
Assert.DoesNotContain(commands, c => c.Text == "Has Name");
}
[Fact]
public void Interpret_IfStringNotEmpty_IsTruthy()
{
var template = Parse("@if Name\nHas Name\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Name": "John"}""");
Assert.Contains(commands, c => c.Text == "Has Name");
}
[Fact]
public void Interpret_IfArrayEmpty_IsFalsy()
{
var template = Parse("@if Items\nHas Items\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": []}""");
Assert.DoesNotContain(commands, c => c.Text == "Has Items");
}
[Fact]
public void Interpret_IfArrayNotEmpty_IsTruthy()
{
var template = Parse("@if Items\nHas Items\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": [1, 2]}""");
Assert.Contains(commands, c => c.Text == "Has Items");
}
[Fact]
public void Interpret_IfNegation_InvertsCondition()
{
var template = Parse("@if !HasValue\nNo Value\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"HasValue": false}""");
Assert.Contains(commands, c => c.Text == "No Value");
}
[Fact]
public void Interpret_IfEqualsComparison_Works()
{
var template = Parse("@if Status == \"active\"\nActive\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Status": "active"}""");
Assert.Contains(commands, c => c.Text == "Active");
}
[Fact]
public void Interpret_IfNotEqualsComparison_Works()
{
var template = Parse("@if Status != \"inactive\"\nNot Inactive\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Status": "active"}""");
Assert.Contains(commands, c => c.Text == "Not Inactive");
}
[Fact]
public void Interpret_IfGreaterThanComparison_Works()
{
var template = Parse("@if Count > 5\nMany Items\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Count": 10}""");
Assert.Contains(commands, c => c.Text == "Many Items");
}
[Fact]
public void Interpret_IfLessThanComparison_Works()
{
var template = Parse("@if Count < 5\nFew Items\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Count": 2}""");
Assert.Contains(commands, c => c.Text == "Few Items");
}
[Fact]
public void Interpret_IfElse_ExecutesElseWhenFalse()
{
var template = Parse("@if HasValue\nYes\n@else\nNo\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"HasValue": false}""");
Assert.DoesNotContain(commands, c => c.Text == "Yes");
Assert.Contains(commands, c => c.Text == "No");
}
[Fact]
public void Interpret_IfElseIf_ExecutesCorrectBranch()
{
var template = Parse("@if Value == 1\nOne\n@elseif Value == 2\nTwo\n@else\nOther\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Value": 2}""");
Assert.DoesNotContain(commands, c => c.Text == "One");
Assert.Contains(commands, c => c.Text == "Two");
Assert.DoesNotContain(commands, c => c.Text == "Other");
}
[Fact]
public void Interpret_ArrayCountComparison_Works()
{
var template = Parse("@if Items.count > 0\nHas Items\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": [1, 2, 3]}""");
Assert.Contains(commands, c => c.Text == "Has Items");
}
}

View File

@@ -0,0 +1,134 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Interpreter;
public class InterpreterLoopTests
{
private readonly PrinterProfile _profile = new("test", "Test Printer", 48, 24, false);
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void Interpret_Foreach_IteratesOverArray()
{
var template = Parse("@foreach item in Items\n{item}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": ["A", "B", "C"]}""");
Assert.Contains(commands, c => c.Text == "A");
Assert.Contains(commands, c => c.Text == "B");
Assert.Contains(commands, c => c.Text == "C");
}
[Fact]
public void Interpret_Foreach_AccessesObjectProperties()
{
var template = Parse("@foreach item in Items\n{item.Name}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": [{"Name": "First"}, {"Name": "Second"}]}""");
Assert.Contains(commands, c => c.Text == "First");
Assert.Contains(commands, c => c.Text == "Second");
}
[Fact]
public void Interpret_Foreach_EmptyArray_NoOutput()
{
var template = Parse("@foreach item in Items\n{item}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": []}""");
Assert.DoesNotContain(commands, c => !string.IsNullOrWhiteSpace(c.Text));
}
[Fact]
public void Interpret_Foreach_IndexMetadata_Works()
{
var template = Parse("@foreach item in Items\n{_index}:{item}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": ["A", "B"]}""");
Assert.Contains(commands, c => c.Text == "0");
Assert.Contains(commands, c => c.Text == "1");
}
[Fact]
public void Interpret_Foreach_NumberMetadata_Works()
{
var template = Parse("@foreach item in Items\n{_number}. {item}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": ["A", "B"]}""");
Assert.Contains(commands, c => c.Text == "1");
Assert.Contains(commands, c => c.Text == "2");
}
[Fact]
public void Interpret_Foreach_FirstMetadata_Works()
{
var template = Parse("@foreach item in Items\n@if _first\nFirst: {item}\n@end\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": ["A", "B", "C"]}""");
var firstCmds = commands.Where(c => c.Text.Contains("First")).ToList();
Assert.Single(firstCmds);
Assert.Contains(commands, c => c.Text == "A");
}
[Fact]
public void Interpret_Foreach_LastMetadata_Works()
{
var template = Parse("@foreach item in Items\n@if _last\nLast: {item}\n@end\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Items": ["A", "B", "C"]}""");
Assert.Contains(commands, c => c.Text.Contains("Last"));
Assert.Contains(commands, c => c.Text == "C");
}
[Fact]
public void Interpret_NestedForeach_Works()
{
var template = Parse("@foreach group in Groups\n@foreach item in group.Items\n{item}\n@end\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template,
"""{"Groups": [{"Items": ["A", "B"]}, {"Items": ["C", "D"]}]}""");
Assert.Contains(commands, c => c.Text == "A");
Assert.Contains(commands, c => c.Text == "B");
Assert.Contains(commands, c => c.Text == "C");
Assert.Contains(commands, c => c.Text == "D");
}
[Fact]
public void Interpret_Foreach_AccessesParentContext()
{
var template = Parse("@foreach item in Items\n{Title}: {item}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Title": "List", "Items": ["A", "B"]}""");
Assert.Contains(commands, c => c.Text == "List");
Assert.Contains(commands, c => c.Text == "A");
Assert.Contains(commands, c => c.Text == "B");
}
[Fact]
public void Interpret_Foreach_NestedPath_Works()
{
var template = Parse("@foreach item in Data.Items\n{item.Name}\n@end");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template,
"""{"Data": {"Items": [{"Name": "First"}, {"Name": "Second"}]}}""");
Assert.Contains(commands, c => c.Text == "First");
Assert.Contains(commands, c => c.Text == "Second");
}
}

View File

@@ -0,0 +1,105 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Interpreter;
public class InterpreterRowTests
{
private readonly PrinterProfile _profile = new("test", "Test Printer", 48, 24, false);
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void Interpret_Row_CombinesColumns()
{
var template = Parse("@row\n|20|Name|10|Value\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
// Should have a single command with combined column text
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Name") && c.Text.Contains("Value"));
Assert.NotNull(rowCmd);
}
[Fact]
public void Interpret_Row_LeftAlignsPaddsRight()
{
var template = Parse("@row\n|10|Hi\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Hi"));
Assert.NotNull(rowCmd);
Assert.Equal(10, rowCmd.Text.Length);
Assert.StartsWith("Hi", rowCmd.Text);
}
[Fact]
public void Interpret_Row_RightAlignsPaddsLeft()
{
var template = Parse("@row\n|10,right|Hi\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Hi"));
Assert.NotNull(rowCmd);
Assert.Equal(10, rowCmd.Text.Length);
Assert.EndsWith("Hi", rowCmd.Text);
}
[Fact]
public void Interpret_Row_CenterAlignsCentersText()
{
var template = Parse("@row\n|10,center|Hi\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Hi"));
Assert.NotNull(rowCmd);
Assert.Equal(10, rowCmd.Text.Length);
Assert.StartsWith(" Hi", rowCmd.Text);
}
[Fact]
public void Interpret_Row_TruncatesLongText()
{
var template = Parse("@row\n|5|HelloWorld\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Hello"));
Assert.NotNull(rowCmd);
Assert.Equal(5, rowCmd.Text.Length);
Assert.Equal("Hello", rowCmd.Text);
}
[Fact]
public void Interpret_Row_WithBindings_ResolvesValues()
{
var template = Parse("@row\n|20|{Name}|10,right|{Price:F2}\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, """{"Name": "Item", "Price": 9.99}""");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("Item") && c.Text.Contains("9.99"));
Assert.NotNull(rowCmd);
}
[Fact]
public void Interpret_Row_MultipleColumns_CorrectTotalWidth()
{
var template = Parse("@row\n|10|A|10|B|10|C\n@endrow");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var rowCmd = commands.FirstOrDefault(c => c.Text.Contains("A") && c.Text.Contains("B") && c.Text.Contains("C"));
Assert.NotNull(rowCmd);
Assert.Equal(30, rowCmd.Text.Length);
}
}

View File

@@ -0,0 +1,153 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Interpreter;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Tests.Interpreter;
public class InterpreterTextTests
{
private readonly PrinterProfile _profile = new("test", "Test Printer", 48, 24, false);
private TemplateNode Parse(string source)
{
var lexer = new Lexer(source);
var parser = new Parser(lexer.Tokenize().Tokens);
return parser.Parse().Template;
}
[Fact]
public void Interpret_PlainText_OutputsText()
{
var template = Parse("Hello World");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
Assert.Contains(commands, c => c.Text == "Hello World");
}
[Fact]
public void Interpret_BoldStyle_SetsBoldFlag()
{
var template = Parse("#bold# text #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.True(cmd.IsBold);
}
[Fact]
public void Interpret_BigStyle_SetsBigFlag()
{
var template = Parse("#big# text #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.True(cmd.IsBig);
}
[Fact]
public void Interpret_TallStyle_SetsTallFlag()
{
var template = Parse("#tall# text #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.True(cmd.IsTall);
}
[Fact]
public void Interpret_RedStyle_SetsRedFlag_WhenSupported()
{
var profileWithRed = new PrinterProfile("test", "Test", 48, 24, true);
var template = Parse("#red# text #");
var interpreter = new TemplateInterpreter(profileWithRed);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.True(cmd.IsRed);
}
[Fact]
public void Interpret_RedStyle_IgnoresRedFlag_WhenNotSupported()
{
var profileNoRed = new PrinterProfile("test", "Test", 48, 24, false);
var template = Parse("#red# text #");
var interpreter = new TemplateInterpreter(profileNoRed);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.False(cmd.IsRed);
}
[Fact]
public void Interpret_CenterStyle_CentersText()
{
var template = Parse("#center# Hi #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("Hi"));
Assert.NotNull(cmd);
Assert.StartsWith(" ", cmd.Text);
}
[Fact]
public void Interpret_MultipleStyles_CombinesFlags()
{
var template = Parse("#bold,big,tall# text #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.True(cmd.IsBold);
Assert.True(cmd.IsBig);
Assert.True(cmd.IsTall);
}
[Fact]
public void Interpret_DashSeparator_OutputsFullWidthLine()
{
var template = Parse("---");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains('-'));
Assert.NotNull(cmd);
Assert.Equal(48, cmd.Text.Length);
Assert.True(cmd.Text.All(c => c == '-'));
}
[Fact]
public void Interpret_EqualsSeparator_OutputsFullWidthLine()
{
var template = Parse("===");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains('='));
Assert.NotNull(cmd);
Assert.Equal(48, cmd.Text.Length);
}
[Fact]
public void Interpret_SpacingStyle_SetsLineSpacing()
{
var template = Parse("#spacing:50# text #");
var interpreter = new TemplateInterpreter(_profile);
var commands = interpreter.Interpret(template, "{}");
var cmd = commands.FirstOrDefault(c => c.Text.Contains("text"));
Assert.NotNull(cmd);
Assert.Equal(50, cmd.SetLineSpacing);
}
}

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);
}
}