229 lines
7.2 KiB
C#
229 lines
7.2 KiB
C#
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;
|
|
}
|
|
}
|