template engine
This commit is contained in:
123
Inspectron.Epson.TemplateEngine/DataBinding/DataContext.cs
Normal file
123
Inspectron.Epson.TemplateEngine/DataBinding/DataContext.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
|
||||
public class DataContext
|
||||
{
|
||||
private readonly JsonElement _root;
|
||||
private readonly DataContext? _parent;
|
||||
private readonly Dictionary<string, JsonElement> _variables = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, object> _loopVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public DataContext(JsonElement root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
private DataContext(JsonElement root, DataContext parent)
|
||||
{
|
||||
_root = root;
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public DataContext CreateChildScope()
|
||||
{
|
||||
return new DataContext(_root, this);
|
||||
}
|
||||
|
||||
public void SetVariable(string name, JsonElement value)
|
||||
{
|
||||
_variables[name] = value;
|
||||
}
|
||||
|
||||
public void SetLoopVariable(string name, object value)
|
||||
{
|
||||
_loopVariables[name] = value;
|
||||
}
|
||||
|
||||
public object? GetLoopVariable(string name)
|
||||
{
|
||||
if (_loopVariables.TryGetValue(name, out var value))
|
||||
return value;
|
||||
return _parent?.GetLoopVariable(name);
|
||||
}
|
||||
|
||||
public JsonElement? Resolve(string expression)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
return null;
|
||||
|
||||
var parts = expression.Split('.');
|
||||
var firstPart = parts[0];
|
||||
|
||||
// Check local variables first
|
||||
JsonElement? current = null;
|
||||
|
||||
if (_variables.TryGetValue(firstPart, out var varValue))
|
||||
{
|
||||
current = varValue;
|
||||
}
|
||||
else if (_parent != null)
|
||||
{
|
||||
// Walk up scope chain for variables
|
||||
var ctx = _parent;
|
||||
while (ctx != null)
|
||||
{
|
||||
if (ctx._variables.TryGetValue(firstPart, out var parentVar))
|
||||
{
|
||||
current = parentVar;
|
||||
break;
|
||||
}
|
||||
ctx = ctx._parent;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in variables, try root
|
||||
if (current == null)
|
||||
{
|
||||
current = GetProperty(_root, firstPart);
|
||||
}
|
||||
|
||||
if (current == null)
|
||||
return null;
|
||||
|
||||
// Navigate remaining parts
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
current = GetProperty(current.Value, parts[i]);
|
||||
if (current == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static JsonElement? GetProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
// Case-insensitive property lookup
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase))
|
||||
return prop.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetStringValue(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString() ?? "",
|
||||
JsonValueKind.Number => element.TryGetInt64(out var l) ? l.ToString() : element.GetDouble().ToString(),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
JsonValueKind.Null => "",
|
||||
JsonValueKind.Undefined => "",
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
|
||||
public class ExpressionEvaluator
|
||||
{
|
||||
private static readonly Regex ExpressionPattern = new(@"\{\{(.+?)\}\}", RegexOptions.Compiled);
|
||||
|
||||
private readonly DataContext _context;
|
||||
|
||||
public ExpressionEvaluator(DataContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public string Evaluate(string template)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
return "";
|
||||
|
||||
return ExpressionPattern.Replace(template, match =>
|
||||
{
|
||||
var expression = match.Groups[1].Value.Trim();
|
||||
return ResolveExpression(expression);
|
||||
});
|
||||
}
|
||||
|
||||
private string ResolveExpression(string expression)
|
||||
{
|
||||
// Loop variables: $index, $first, $last
|
||||
if (expression.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(expression);
|
||||
return loopVar?.ToString() ?? "";
|
||||
}
|
||||
|
||||
// Format string: PropertyName:format
|
||||
string? format = null;
|
||||
var colonIndex = expression.IndexOf(':');
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
format = expression[(colonIndex + 1)..];
|
||||
expression = expression[..colonIndex];
|
||||
}
|
||||
|
||||
var element = _context.Resolve(expression);
|
||||
if (element == null)
|
||||
return "";
|
||||
|
||||
if (format != null)
|
||||
return FormatValue(element.Value, format);
|
||||
|
||||
return DataContext.GetStringValue(element.Value);
|
||||
}
|
||||
|
||||
private static string FormatValue(JsonElement element, string format)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
if (str != null && DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt))
|
||||
{
|
||||
return dt.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
return str ?? "";
|
||||
}
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (element.TryGetDecimal(out var d))
|
||||
return d.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return DataContext.GetStringValue(element);
|
||||
}
|
||||
|
||||
public bool EvaluateCondition(string test)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(test))
|
||||
return false;
|
||||
|
||||
// Negation
|
||||
bool negate = false;
|
||||
var expr = test.Trim();
|
||||
if (expr.StartsWith("!"))
|
||||
{
|
||||
negate = true;
|
||||
expr = expr[1..].Trim();
|
||||
}
|
||||
|
||||
// Loop variables: $first, $last
|
||||
if (expr.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(expr);
|
||||
bool loopResult = loopVar is bool b ? b : loopVar != null;
|
||||
return negate ? !loopResult : loopResult;
|
||||
}
|
||||
|
||||
// Comparison operators
|
||||
var comparisonOps = new[] { "==", "!=", ">=", "<=", ">", "<" };
|
||||
foreach (var op in comparisonOps)
|
||||
{
|
||||
var parts = SplitComparison(expr, op);
|
||||
if (parts != null)
|
||||
{
|
||||
bool compResult = EvaluateComparison(parts.Value.left, op, parts.Value.right);
|
||||
return negate ? !compResult : compResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Truthiness check - property exists and has a value
|
||||
var result = IsTruthy(expr);
|
||||
return negate ? !result : result;
|
||||
}
|
||||
|
||||
private (string left, string right)? SplitComparison(string expr, string op)
|
||||
{
|
||||
var idx = expr.IndexOf(op, StringComparison.Ordinal);
|
||||
if (idx < 0) return null;
|
||||
|
||||
// Make sure we don't confuse == with = or != with !
|
||||
if (op == "=" && idx > 0 && expr[idx - 1] == '!') return null;
|
||||
if (op == ">" && idx > 0 && (expr[idx - 1] == '>' || expr[idx - 1] == '<')) return null;
|
||||
|
||||
var left = expr[..idx].Trim();
|
||||
var right = expr[(idx + op.Length)..].Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right))
|
||||
return null;
|
||||
|
||||
return (left, right);
|
||||
}
|
||||
|
||||
private bool EvaluateComparison(string left, string op, string right)
|
||||
{
|
||||
var leftVal = ResolveComparisonValue(left);
|
||||
var rightVal = ResolveComparisonValue(right);
|
||||
|
||||
// Try numeric comparison
|
||||
if (decimal.TryParse(leftVal, CultureInfo.InvariantCulture, out var leftNum)
|
||||
&& decimal.TryParse(rightVal, CultureInfo.InvariantCulture, out var rightNum))
|
||||
{
|
||||
return op switch
|
||||
{
|
||||
"==" => leftNum == rightNum,
|
||||
"!=" => leftNum != rightNum,
|
||||
">" => leftNum > rightNum,
|
||||
"<" => leftNum < rightNum,
|
||||
">=" => leftNum >= rightNum,
|
||||
"<=" => leftNum <= rightNum,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
// String comparison
|
||||
int cmp = string.Compare(leftVal, rightVal, StringComparison.OrdinalIgnoreCase);
|
||||
return op switch
|
||||
{
|
||||
"==" => cmp == 0,
|
||||
"!=" => cmp != 0,
|
||||
">" => cmp > 0,
|
||||
"<" => cmp < 0,
|
||||
">=" => cmp >= 0,
|
||||
"<=" => cmp <= 0,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveComparisonValue(string value)
|
||||
{
|
||||
// Quoted string literal
|
||||
if ((value.StartsWith("'") && value.EndsWith("'")) ||
|
||||
(value.StartsWith("\"") && value.EndsWith("\"")))
|
||||
{
|
||||
return value[1..^1];
|
||||
}
|
||||
|
||||
// Numeric literal
|
||||
if (decimal.TryParse(value, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Loop variable
|
||||
if (value.StartsWith("$"))
|
||||
{
|
||||
var loopVar = _context.GetLoopVariable(value);
|
||||
return loopVar?.ToString() ?? "";
|
||||
}
|
||||
|
||||
// Property resolution
|
||||
var element = _context.Resolve(value);
|
||||
if (element == null)
|
||||
return "";
|
||||
|
||||
return DataContext.GetStringValue(element.Value);
|
||||
}
|
||||
|
||||
private bool IsTruthy(string expression)
|
||||
{
|
||||
var element = _context.Resolve(expression);
|
||||
if (element == null)
|
||||
return false;
|
||||
|
||||
return element.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Null => false,
|
||||
JsonValueKind.Undefined => false,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.String => !string.IsNullOrEmpty(element.Value.GetString()),
|
||||
JsonValueKind.Number => element.Value.GetDouble() != 0,
|
||||
JsonValueKind.Array => element.Value.GetArrayLength() > 0,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user