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,28 @@
using Inspectron.Epson.Templates.Language;
namespace Inspectron.Epson.Templates.Validation;
public enum TemplateErrorCode
{
// Syntax errors (RTL001-RTL010)
RTL001_UnexpectedToken = 1,
RTL002_UnclosedBlock = 2,
RTL003_InvalidDirective = 3,
RTL004_InvalidBinding = 4,
RTL005_InvalidForeachSyntax = 5,
RTL006_InvalidCondition = 6,
RTL007_UnclosedStyleBlock = 7,
RTL008_InvalidColumnSpec = 8,
RTL009_InvalidSeparator = 9,
RTL010_UnexpectedEndOfFile = 10
}
public record TemplateError(
TemplateErrorCode Code,
string Message,
SourcePosition Position)
{
public string CodeString => Code.ToString().Split('_')[0];
public override string ToString() => $"{CodeString}: {Message} at {Position}";
}

View File

@@ -0,0 +1,35 @@
namespace Inspectron.Epson.Templates.Validation;
public record TemplateValidationResult(
List<TemplateError> Errors,
List<TemplateWarning> Warnings)
{
public bool IsValid => Errors.Count == 0;
public bool HasWarnings => Warnings.Count > 0;
public static TemplateValidationResult Valid() => new(new List<TemplateError>(), new List<TemplateWarning>());
public static TemplateValidationResult WithErrors(params TemplateError[] errors) =>
new(errors.ToList(), new List<TemplateWarning>());
public override string ToString()
{
if (IsValid && !HasWarnings)
{
return "Template is valid.";
}
var lines = new List<string>();
if (Errors.Count > 0)
{
lines.Add($"Errors ({Errors.Count}):");
lines.AddRange(Errors.Select(e => $" {e}"));
}
if (Warnings.Count > 0)
{
lines.Add($"Warnings ({Warnings.Count}):");
lines.AddRange(Warnings.Select(w => $" {w}"));
}
return string.Join(Environment.NewLine, lines);
}
}

View File

@@ -0,0 +1,228 @@
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Validation;
public class TemplateValidator
{
private readonly PrinterProfile? _profile;
private readonly List<TemplateError> _errors = new();
private readonly List<TemplateWarning> _warnings = new();
private readonly HashSet<string> _definedVariables = new();
public TemplateValidator(PrinterProfile? profile = null)
{
_profile = profile;
}
public TemplateValidationResult Validate(string templateSource)
{
_errors.Clear();
_warnings.Clear();
_definedVariables.Clear();
// Lexer phase
var lexer = new Lexer(templateSource);
var lexerResult = lexer.Tokenize();
foreach (var error in lexerResult.Errors)
{
_errors.Add(new TemplateError(
TemplateErrorCode.RTL007_UnclosedStyleBlock,
error.Message,
error.Position));
}
// Parser phase
var parser = new Parser(lexerResult.Tokens);
var parseResult = parser.Parse();
foreach (var error in parseResult.Errors)
{
var code = MapParseError(error.Message);
_errors.Add(new TemplateError(code, error.Message, error.Position));
}
// Semantic validation
if (_errors.Count == 0)
{
ValidateNode(parseResult.Template);
}
return new TemplateValidationResult(_errors.ToList(), _warnings.ToList());
}
private void ValidateNode(ITemplateNode node)
{
switch (node)
{
case TemplateNode template:
foreach (var child in template.ChildNodes)
{
ValidateNode(child);
}
break;
case IfNode ifNode:
ValidateCondition(ifNode.IfBranch.Condition, ifNode.Position);
if (ifNode.IfBranch.Body.Count == 0)
{
_warnings.Add(new TemplateWarning(
TemplateWarningCode.RTL100_EmptyBlock,
"Empty @if block body",
ifNode.Position));
}
foreach (var child in ifNode.IfBranch.Body)
{
ValidateNode(child);
}
foreach (var branch in ifNode.ElseIfBranches)
{
ValidateCondition(branch.Condition, branch.Position);
foreach (var child in branch.Body)
{
ValidateNode(child);
}
}
if (ifNode.ElseBranch != null)
{
foreach (var child in ifNode.ElseBranch)
{
ValidateNode(child);
}
}
break;
case ForeachNode foreachNode:
_definedVariables.Add(foreachNode.ItemVariable);
if (foreachNode.Body.Count == 0)
{
_warnings.Add(new TemplateWarning(
TemplateWarningCode.RTL100_EmptyBlock,
"Empty @foreach block body",
foreachNode.Position));
}
foreach (var child in foreachNode.Body)
{
ValidateNode(child);
}
_definedVariables.Remove(foreachNode.ItemVariable);
break;
case RowNode rowNode:
ValidateRowWidth(rowNode);
foreach (var column in rowNode.Columns)
{
ValidateNode(column);
}
break;
case ColumnNode columnNode:
foreach (var child in columnNode.ContentNodes)
{
ValidateNode(child);
}
break;
case StyledTextNode styledNode:
ValidateStyles(styledNode.Styles, styledNode.Position);
foreach (var child in styledNode.ContentNodes)
{
ValidateNode(child);
}
break;
case BindingNode bindingNode:
ValidateBinding(bindingNode);
break;
case TextNode:
case SeparatorNode:
case EmptyLineNode:
// No additional validation needed
break;
}
}
private void ValidateCondition(string condition, SourcePosition position)
{
if (string.IsNullOrWhiteSpace(condition))
{
_errors.Add(new TemplateError(
TemplateErrorCode.RTL006_InvalidCondition,
"Empty condition expression",
position));
}
}
private void ValidateBinding(BindingNode node)
{
if (string.IsNullOrWhiteSpace(node.Path))
{
_errors.Add(new TemplateError(
TemplateErrorCode.RTL004_InvalidBinding,
"Empty binding path",
node.Position));
}
}
private void ValidateStyles(IReadOnlyList<string> styles, SourcePosition position)
{
var validStyles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"bold", "big", "tall", "red", "center", "right", "left"
};
foreach (var style in styles)
{
var styleName = style.ToLowerInvariant();
if (styleName.StartsWith("spacing:"))
{
var spacingValue = styleName[8..];
if (!int.TryParse(spacingValue, out _))
{
_warnings.Add(new TemplateWarning(
TemplateWarningCode.RTL102_PossibleNullReference,
$"Invalid spacing value: '{spacingValue}'",
position));
}
continue;
}
if (!validStyles.Contains(styleName))
{
_warnings.Add(new TemplateWarning(
TemplateWarningCode.RTL102_PossibleNullReference,
$"Unknown style: '{style}'",
position));
}
}
}
private void ValidateRowWidth(RowNode row)
{
if (_profile == null) return;
var totalWidth = row.Columns.Sum(c => c.Width);
if (totalWidth > _profile.LineWidth)
{
_warnings.Add(new TemplateWarning(
TemplateWarningCode.RTL103_LineWidthExceeded,
$"Row total width ({totalWidth}) exceeds printer line width ({_profile.LineWidth})",
row.Position));
}
}
private TemplateErrorCode MapParseError(string message)
{
if (message.Contains("@end"))
return TemplateErrorCode.RTL002_UnclosedBlock;
if (message.Contains("foreach"))
return TemplateErrorCode.RTL005_InvalidForeachSyntax;
if (message.Contains("unexpected", StringComparison.OrdinalIgnoreCase))
return TemplateErrorCode.RTL001_UnexpectedToken;
return TemplateErrorCode.RTL001_UnexpectedToken;
}
}

View File

@@ -0,0 +1,22 @@
using Inspectron.Epson.Templates.Language;
namespace Inspectron.Epson.Templates.Validation;
public enum TemplateWarningCode
{
// Semantic warnings (RTL100-RTL103)
RTL100_EmptyBlock = 100,
RTL101_UnusedVariable = 101,
RTL102_PossibleNullReference = 102,
RTL103_LineWidthExceeded = 103
}
public record TemplateWarning(
TemplateWarningCode Code,
string Message,
SourcePosition Position)
{
public string CodeString => Code.ToString().Split('_')[0];
public override string ToString() => $"{CodeString}: {Message} at {Position}";
}