templates support
This commit is contained in:
213
Inspectron.Epson.Templates/Interpreter/DataContext.cs
Normal file
213
Inspectron.Epson.Templates/Interpreter/DataContext.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
176
Inspectron.Epson.Templates/Interpreter/ExpressionEvaluator.cs
Normal file
176
Inspectron.Epson.Templates/Interpreter/ExpressionEvaluator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
129
Inspectron.Epson.Templates/Interpreter/FormatResolver.cs
Normal file
129
Inspectron.Epson.Templates/Interpreter/FormatResolver.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
428
Inspectron.Epson.Templates/Interpreter/TemplateInterpreter.cs
Normal file
428
Inspectron.Epson.Templates/Interpreter/TemplateInterpreter.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user