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,213 @@
using System.Text.Json;
namespace Inspectron.Epson.Templates.Interpreter;
public class DataContext
{
private readonly JsonElement _root;
private readonly DataContext? _parent;
private readonly Dictionary<string, JsonElement> _localVariables = new();
private readonly Dictionary<string, object> _loopMetadata = new();
public DataContext(JsonElement root)
{
_root = root;
_parent = null;
}
private DataContext(JsonElement root, DataContext parent)
{
_root = root;
_parent = parent;
}
public DataContext CreateChildContext(string variableName, JsonElement value, int index, int count)
{
var child = new DataContext(_root, this);
child._localVariables[variableName] = value;
child._loopMetadata["_index"] = index;
child._loopMetadata["_number"] = index + 1;
child._loopMetadata["_first"] = index == 0;
child._loopMetadata["_last"] = index == count - 1;
child._loopMetadata["_count"] = count;
return child;
}
public JsonElement? Resolve(string path)
{
if (string.IsNullOrEmpty(path))
{
return _root;
}
// Check loop metadata first
if (_loopMetadata.TryGetValue(path, out var metadata))
{
return JsonSerializer.SerializeToElement(metadata);
}
var segments = path.Split('.');
var firstSegment = segments[0];
// Check local variables (loop variables)
if (_localVariables.TryGetValue(firstSegment, out var localValue))
{
return NavigatePath(localValue, segments.Skip(1).ToArray());
}
// Check parent context for local variables
if (_parent != null)
{
// First check if parent has this as a local variable
var parentResult = _parent.ResolveLocalOnly(firstSegment);
if (parentResult.HasValue)
{
return NavigatePath(parentResult.Value, segments.Skip(1).ToArray());
}
// Also check parent's loop metadata
if (_parent._loopMetadata.TryGetValue(path, out var parentMetadata))
{
return JsonSerializer.SerializeToElement(parentMetadata);
}
}
// Navigate from root
return NavigatePath(_root, segments);
}
private JsonElement? ResolveLocalOnly(string name)
{
if (_localVariables.TryGetValue(name, out var value))
{
return value;
}
return _parent?.ResolveLocalOnly(name);
}
private JsonElement? NavigatePath(JsonElement element, string[] segments)
{
var current = element;
foreach (var segment in segments)
{
if (current.ValueKind == JsonValueKind.Null || current.ValueKind == JsonValueKind.Undefined)
{
return null;
}
// Handle array access: collection.count or collection.length
if (segment.Equals("count", StringComparison.OrdinalIgnoreCase) ||
segment.Equals("length", StringComparison.OrdinalIgnoreCase))
{
if (current.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.SerializeToElement(current.GetArrayLength());
}
}
// Handle array index: collection.0, collection.1
if (int.TryParse(segment, out var index))
{
if (current.ValueKind == JsonValueKind.Array && index >= 0 && index < current.GetArrayLength())
{
current = current[index];
continue;
}
return null;
}
// Handle object property
if (current.ValueKind == JsonValueKind.Object)
{
if (current.TryGetProperty(segment, out var property))
{
current = property;
}
else
{
// Try case-insensitive match
var found = false;
foreach (var prop in current.EnumerateObject())
{
if (prop.Name.Equals(segment, StringComparison.OrdinalIgnoreCase))
{
current = prop.Value;
found = true;
break;
}
}
if (!found) return null;
}
}
else
{
return null;
}
}
return current;
}
public IEnumerable<JsonElement> ResolveCollection(string path)
{
var element = Resolve(path);
if (element.HasValue && element.Value.ValueKind == JsonValueKind.Array)
{
foreach (var item in element.Value.EnumerateArray())
{
yield return item;
}
}
}
public string? GetString(string path)
{
var element = Resolve(path);
if (!element.HasValue) return null;
return element.Value.ValueKind switch
{
JsonValueKind.String => element.Value.GetString(),
JsonValueKind.Number => element.Value.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => null,
_ => element.Value.GetRawText()
};
}
public bool IsTruthy(string path)
{
var element = Resolve(path);
if (!element.HasValue) return false;
return element.Value.ValueKind switch
{
JsonValueKind.Null or JsonValueKind.Undefined => false,
JsonValueKind.False => false,
JsonValueKind.True => true,
JsonValueKind.String => !string.IsNullOrEmpty(element.Value.GetString()),
JsonValueKind.Number => element.Value.GetDouble() != 0,
JsonValueKind.Array => element.Value.GetArrayLength() > 0,
JsonValueKind.Object => true,
_ => false
};
}
public object? GetValue(string path)
{
var element = Resolve(path);
if (!element.HasValue) return null;
return element.Value.ValueKind switch
{
JsonValueKind.String => element.Value.GetString(),
JsonValueKind.Number => element.Value.TryGetInt64(out var l) ? l : element.Value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.Value.GetRawText()
};
}
}

View File

@@ -0,0 +1,176 @@
using System.Text.RegularExpressions;
namespace Inspectron.Epson.Templates.Interpreter;
public class ExpressionEvaluator
{
private static readonly Regex ComparisonRegex = new(
@"^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$",
RegexOptions.Compiled);
public bool Evaluate(string expression, DataContext context)
{
expression = expression.Trim();
if (string.IsNullOrEmpty(expression))
{
return false;
}
// Handle negation: !property
if (expression.StartsWith("!"))
{
var inner = expression[1..].Trim();
return !Evaluate(inner, context);
}
// Handle comparison operators
var match = ComparisonRegex.Match(expression);
if (match.Success)
{
var left = match.Groups[1].Value.Trim();
var op = match.Groups[2].Value;
var right = match.Groups[3].Value.Trim();
return EvaluateComparison(left, op, right, context);
}
// Handle logical AND: property1 && property2
if (expression.Contains("&&"))
{
var parts = expression.Split("&&", 2);
return Evaluate(parts[0], context) && Evaluate(parts[1], context);
}
// Handle logical OR: property1 || property2
if (expression.Contains("||"))
{
var parts = expression.Split("||", 2);
return Evaluate(parts[0], context) || Evaluate(parts[1], context);
}
// Simple truthy check
return context.IsTruthy(expression);
}
private bool EvaluateComparison(string left, string op, string right, DataContext context)
{
var leftValue = ResolveValue(left, context);
var rightValue = ResolveValue(right, context);
// Handle null comparisons
if (leftValue == null && rightValue == null)
{
return op == "==" || op == ">=" || op == "<=";
}
if (leftValue == null || rightValue == null)
{
return op switch
{
"==" => false,
"!=" => true,
_ => false
};
}
// Try numeric comparison first
if (TryGetNumeric(leftValue, out var leftNum) && TryGetNumeric(rightValue, out var rightNum))
{
return op switch
{
"==" => Math.Abs(leftNum - rightNum) < 0.0001,
"!=" => Math.Abs(leftNum - rightNum) >= 0.0001,
">" => leftNum > rightNum,
"<" => leftNum < rightNum,
">=" => leftNum >= rightNum,
"<=" => leftNum <= rightNum,
_ => false
};
}
// Fall back to string comparison
var leftStr = leftValue.ToString() ?? "";
var rightStr = rightValue.ToString() ?? "";
return op switch
{
"==" => leftStr.Equals(rightStr, StringComparison.OrdinalIgnoreCase),
"!=" => !leftStr.Equals(rightStr, StringComparison.OrdinalIgnoreCase),
">" => string.Compare(leftStr, rightStr, StringComparison.OrdinalIgnoreCase) > 0,
"<" => string.Compare(leftStr, rightStr, StringComparison.OrdinalIgnoreCase) < 0,
">=" => string.Compare(leftStr, rightStr, StringComparison.OrdinalIgnoreCase) >= 0,
"<=" => string.Compare(leftStr, rightStr, StringComparison.OrdinalIgnoreCase) <= 0,
_ => false
};
}
private object? ResolveValue(string expression, DataContext context)
{
expression = expression.Trim();
// Check for quoted string literal
if ((expression.StartsWith("\"") && expression.EndsWith("\"")) ||
(expression.StartsWith("'") && expression.EndsWith("'")))
{
return expression[1..^1];
}
// Check for numeric literal
if (double.TryParse(expression, out var num))
{
return num;
}
// Check for boolean literal
if (expression.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (expression.Equals("false", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Check for null literal
if (expression.Equals("null", StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Resolve as path
return context.GetValue(expression);
}
private bool TryGetNumeric(object? value, out double result)
{
result = 0;
if (value == null) return false;
if (value is double d)
{
result = d;
return true;
}
if (value is long l)
{
result = l;
return true;
}
if (value is int i)
{
result = i;
return true;
}
if (value is string s && double.TryParse(s, out result))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,129 @@
using System.Globalization;
using System.Text.Json;
namespace Inspectron.Epson.Templates.Interpreter;
public class FormatResolver
{
private readonly CultureInfo _culture;
public FormatResolver(CultureInfo? culture = null)
{
_culture = culture ?? CultureInfo.InvariantCulture;
}
public string Format(JsonElement? element, string? format)
{
if (!element.HasValue || element.Value.ValueKind == JsonValueKind.Null)
{
return string.Empty;
}
var value = element.Value;
if (string.IsNullOrEmpty(format))
{
return GetDefaultString(value);
}
return value.ValueKind switch
{
JsonValueKind.Number => FormatNumber(value, format),
JsonValueKind.String => FormatString(value.GetString(), format),
_ => GetDefaultString(value)
};
}
private string FormatNumber(JsonElement value, string format)
{
// Try to get as decimal for precision
if (value.TryGetDecimal(out var decimalValue))
{
try
{
return decimalValue.ToString(format, _culture);
}
catch (FormatException)
{
return decimalValue.ToString(_culture);
}
}
if (value.TryGetDouble(out var doubleValue))
{
try
{
return doubleValue.ToString(format, _culture);
}
catch (FormatException)
{
return doubleValue.ToString(_culture);
}
}
return value.GetRawText();
}
private string FormatString(string? value, string format)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
// Try to parse as DateTime
if (DateTime.TryParse(value, out var dateTime))
{
try
{
return dateTime.ToString(format, _culture);
}
catch (FormatException)
{
return value;
}
}
// Try to parse as DateTimeOffset (for ISO 8601 strings)
if (DateTimeOffset.TryParse(value, out var dateTimeOffset))
{
try
{
return dateTimeOffset.ToString(format, _culture);
}
catch (FormatException)
{
return value;
}
}
// If it's a number in string form
if (decimal.TryParse(value, out var decimalValue))
{
try
{
return decimalValue.ToString(format, _culture);
}
catch (FormatException)
{
return value;
}
}
return value;
}
private string GetDefaultString(JsonElement value)
{
return value.ValueKind switch
{
JsonValueKind.String => value.GetString() ?? string.Empty,
JsonValueKind.Number => value.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => string.Empty,
JsonValueKind.Undefined => string.Empty,
_ => value.GetRawText()
};
}
}

View File

@@ -0,0 +1,20 @@
using Inspectron.Epson.Templates.Language;
namespace Inspectron.Epson.Templates.Interpreter;
public class InterpreterException : Exception
{
public SourcePosition Position { get; }
public InterpreterException(string message, SourcePosition position)
: base($"{message} at {position}")
{
Position = position;
}
public InterpreterException(string message, SourcePosition position, Exception innerException)
: base($"{message} at {position}", innerException)
{
Position = position;
}
}

View File

@@ -0,0 +1,428 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using Inspectron.Epson.PrintServer.Printers.Utils;
using Inspectron.Epson.Templates.Configuration;
using Inspectron.Epson.Templates.Language;
using Inspectron.Epson.Templates.Language.Nodes;
namespace Inspectron.Epson.Templates.Interpreter;
public class TemplateInterpreter
{
private readonly PrinterProfile _profile;
private readonly ExpressionEvaluator _evaluator;
private readonly FormatResolver _formatter;
public TemplateInterpreter(PrinterProfile profile, CultureInfo? culture = null)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
_evaluator = new ExpressionEvaluator();
_formatter = new FormatResolver(culture);
}
public List<PrintCommand> Interpret(TemplateNode template, string jsonData)
{
var document = JsonDocument.Parse(jsonData);
var context = new DataContext(document.RootElement);
return Interpret(template, context);
}
public List<PrintCommand> Interpret(TemplateNode template, DataContext context)
{
var commands = new List<PrintCommand>();
InterpretNodes(template.ChildNodes, context, commands, new StyleContext());
return commands;
}
private void InterpretNodes(
IReadOnlyList<ITemplateNode> nodes,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
foreach (var node in nodes)
{
InterpretNode(node, context, commands, style);
}
}
private void InterpretNode(
ITemplateNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
switch (node)
{
case TextNode textNode:
InterpretText(textNode, context, commands, style);
break;
case BindingNode bindingNode:
InterpretBinding(bindingNode, context, commands, style);
break;
case StyledTextNode styledNode:
InterpretStyledText(styledNode, context, commands, style);
break;
case SeparatorNode separatorNode:
InterpretSeparator(separatorNode, commands, style);
break;
case EmptyLineNode:
commands.Add(CreateCommand(string.Empty, style));
break;
case IfNode ifNode:
InterpretIf(ifNode, context, commands, style);
break;
case ForeachNode foreachNode:
InterpretForeach(foreachNode, context, commands, style);
break;
case RowNode rowNode:
InterpretRow(rowNode, context, commands, style);
break;
case ColumnNode columnNode:
InterpretColumn(columnNode, context, commands, style);
break;
}
}
private void InterpretText(
TextNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var text = ApplyAlignment(node.Text, style);
commands.Add(CreateCommand(text, style));
}
private void InterpretBinding(
BindingNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var element = context.Resolve(node.Path);
var text = _formatter.Format(element, node.Format);
text = ApplyAlignment(text, style);
commands.Add(CreateCommand(text, style));
}
private void InterpretStyledText(
StyledTextNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
// Create new style context with inherited and new styles
var newStyle = style.Clone();
ApplyStyles(node.Styles, newStyle);
// Build content string from child nodes
var contentBuilder = new StringBuilder();
foreach (var child in node.ContentNodes)
{
switch (child)
{
case TextNode textNode:
contentBuilder.Append(textNode.Text);
break;
case BindingNode bindingNode:
var element = context.Resolve(bindingNode.Path);
contentBuilder.Append(_formatter.Format(element, bindingNode.Format));
break;
}
}
var text = contentBuilder.ToString();
text = ApplyAlignment(text, newStyle);
commands.Add(CreateCommand(text, newStyle));
}
private void InterpretSeparator(
SeparatorNode node,
List<PrintCommand> commands,
StyleContext style)
{
var width = style.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
var ch = node.Style switch
{
SeparatorStyle.Dash => '-',
SeparatorStyle.Equals => '=',
SeparatorStyle.Star => '*',
SeparatorStyle.Tilde => '~',
_ => '-'
};
var line = new string(ch, width);
commands.Add(CreateCommand(line, style));
}
private void InterpretIf(
IfNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
// Evaluate @if condition
if (_evaluator.Evaluate(node.IfBranch.Condition, context))
{
InterpretNodes(node.IfBranch.Body, context, commands, style);
return;
}
// Evaluate @elseif conditions
foreach (var branch in node.ElseIfBranches)
{
if (_evaluator.Evaluate(branch.Condition, context))
{
InterpretNodes(branch.Body, context, commands, style);
return;
}
}
// Execute @else branch if present
if (node.ElseBranch != null)
{
InterpretNodes(node.ElseBranch, context, commands, style);
}
}
private void InterpretForeach(
ForeachNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var collection = context.ResolveCollection(node.CollectionPath).ToList();
var count = collection.Count;
for (int i = 0; i < count; i++)
{
var item = collection[i];
var childContext = context.CreateChildContext(node.ItemVariable, item, i, count);
InterpretNodes(node.Body, childContext, commands, style);
}
}
private void InterpretRow(
RowNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var lineWidth = style.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
var rowBuilder = new StringBuilder();
foreach (var column in node.Columns)
{
// Build column content
var columnContent = new StringBuilder();
foreach (var child in column.ContentNodes)
{
switch (child)
{
case TextNode textNode:
columnContent.Append(textNode.Text);
break;
case BindingNode bindingNode:
var element = context.Resolve(bindingNode.Path);
columnContent.Append(_formatter.Format(element, bindingNode.Format));
break;
case StyledTextNode styledNode:
foreach (var styledChild in styledNode.ContentNodes)
{
if (styledChild is TextNode st)
columnContent.Append(st.Text);
else if (styledChild is BindingNode sb)
{
var elem = context.Resolve(sb.Path);
columnContent.Append(_formatter.Format(elem, sb.Format));
}
}
break;
}
}
var content = columnContent.ToString();
var width = column.Width > 0 ? column.Width : content.Length;
// Apply column alignment
var aligned = column.Alignment switch
{
ColumnAlignment.Right => content.PadLeft(width),
ColumnAlignment.Center => CenterText(content, width),
_ => content.PadRight(width)
};
// Truncate if too long
if (aligned.Length > width && width > 0)
{
aligned = aligned[..width];
}
rowBuilder.Append(aligned);
}
commands.Add(CreateCommand(rowBuilder.ToString(), style));
}
private void InterpretColumn(
ColumnNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
// Standalone column - just output content with width constraints
var contentBuilder = new StringBuilder();
foreach (var child in node.ContentNodes)
{
switch (child)
{
case TextNode textNode:
contentBuilder.Append(textNode.Text);
break;
case BindingNode bindingNode:
var element = context.Resolve(bindingNode.Path);
contentBuilder.Append(_formatter.Format(element, bindingNode.Format));
break;
}
}
var content = contentBuilder.ToString();
var width = node.Width > 0 ? node.Width : content.Length;
var aligned = node.Alignment switch
{
ColumnAlignment.Right => content.PadLeft(width),
ColumnAlignment.Center => CenterText(content, width),
_ => content.PadRight(width)
};
if (aligned.Length > width && width > 0)
{
aligned = aligned[..width];
}
commands.Add(CreateCommand(aligned, style));
}
private void ApplyStyles(IReadOnlyList<string> styles, StyleContext styleContext)
{
foreach (var style in styles)
{
var lower = style.ToLowerInvariant();
if (lower.StartsWith("spacing:"))
{
if (int.TryParse(lower[8..], out var spacing))
{
styleContext.LineSpacing = spacing;
}
continue;
}
switch (lower)
{
case "bold":
styleContext.IsBold = true;
break;
case "big":
styleContext.IsBig = true;
break;
case "tall":
styleContext.IsTall = true;
break;
case "red":
styleContext.IsRed = true;
break;
case "center":
styleContext.Alignment = TextAlignment.Center;
break;
case "right":
styleContext.Alignment = TextAlignment.Right;
break;
case "left":
styleContext.Alignment = TextAlignment.Left;
break;
}
}
}
private string ApplyAlignment(string text, StyleContext style)
{
if (style.Alignment == TextAlignment.Left)
{
return text;
}
var width = style.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
return style.Alignment switch
{
TextAlignment.Center => CenterText(text, width),
TextAlignment.Right => text.PadLeft(width),
_ => text
};
}
private string CenterText(string text, int width)
{
if (string.IsNullOrEmpty(text) || text.Length >= width)
{
return text;
}
var totalPadding = width - text.Length;
var leftPadding = totalPadding / 2;
var rightPadding = totalPadding - leftPadding;
return new string(' ', leftPadding) + text + new string(' ', rightPadding);
}
private PrintCommand CreateCommand(string text, StyleContext style)
{
var command = new PrintCommand(text, style.IsBig, style.IsBold)
{
IsTall = style.IsTall,
IsRed = style.IsRed && _profile.SupportsRed,
SetLineSpacing = style.LineSpacing
};
return command;
}
}
internal enum TextAlignment
{
Left,
Center,
Right
}
internal class StyleContext
{
public bool IsBold { get; set; }
public bool IsBig { get; set; }
public bool IsTall { get; set; }
public bool IsRed { get; set; }
public TextAlignment Alignment { get; set; } = TextAlignment.Left;
public int? LineSpacing { get; set; }
public StyleContext Clone()
{
return new StyleContext
{
IsBold = IsBold,
IsBig = IsBig,
IsTall = IsTall,
IsRed = IsRed,
Alignment = Alignment,
LineSpacing = LineSpacing
};
}
}