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 _errors = new(); private readonly List _warnings = new(); private readonly HashSet _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 styles, SourcePosition position) { var validStyles = new HashSet(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; } }