Skip to content

Commit 125a335

Browse files
committed
feat(Rendering): Add custom function support and extend data range for template engine
Added the `RegisterFunctions` method to register custom functions with `AsyncNCalcEngine`, currently including the `ToString` function. Meanwhile, four fields—`everMax`, `everMin`, `currentMax`, and `currentMin`—have been added to the drawing data range to retrieve the maximum and minimum values of rating data in templates. This enhances the expressiveness of templates and enables more flexible data formatting and display.
1 parent 1534aca commit 125a335

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

src/Render/Drawer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecor
3232
[.. currentList.Select((record, idx) => new { Record = record, Index = idx + everList.Count + 1 })];
3333
int everDelta = everList.Count > 34 ? everList[0].DXRating - everList[^1].DXRating : everList.Count > 0 ? everList[0].DXRating : 0;
3434
int currentDelta = currentList.Count > 14 ? currentList[0].DXRating - currentList[^1].DXRating : currentList.Count > 0 ? currentList[0].DXRating : 0;
35+
int everMax = everList.Count > 0 ? everList[0].DXRating : 0;
36+
int everMin = everList.Count > 0 ? everList[^1].DXRating : 0;
37+
int currentMax = currentList.Count > 0 ? currentList[0].DXRating : 0;
38+
int currentMin = currentList.Count > 0 ? currentList[^1].DXRating : 0;
3539
int realRating = everTotal + currentTotal;
3640
string proberState = "on";
3741
if (user.Rating != realRating)
@@ -59,6 +63,10 @@ public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecor
5963
["isAnime"] = isAnime,
6064
["drawLevelSeg"] = drawLevelSeg,
6165
["proberState"] = proberState,
66+
["everMax"] = everMax,
67+
["everMin"] = everMin,
68+
["currentMax"] = currentMax,
69+
["currentMin"] = currentMin,
6270
};
6371
return await DrawAsync(scope, xmlPath);
6472
}
@@ -93,9 +101,15 @@ public async Task<Image> DrawListAsync(CommonUser user, IEnumerable<CommonRecord
93101
private async Task<Image> DrawAsync(Dictionary<string, object?> scope, string xmlPath)
94102
{
95103
AsyncNCalcEngine expr = new();
104+
RegisterFunctions(expr);
96105
TemplateReader loader = new(expr);
97106
Node tree = await loader.LoadAsync(xmlPath, scope);
98107
AssetProvider assets = AssetProvider.Shared;
99108
return NodeRenderer.Render((CanvasNode)tree, assets, assets);
100109
}
110+
111+
private void RegisterFunctions(AsyncNCalcEngine expr)
112+
{
113+
expr.RegisterFunction("ToString", (object x) => Convert.ToString(x));
114+
}
101115
}

src/Render/ExpressionEngine/AsyncNCalcEngine.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
using NCalc;
22
using System.Collections;
3+
using System.Collections.Concurrent;
34
using System.Globalization;
5+
using System.Reflection;
46

57
namespace Limekuma.Render.ExpressionEngine;
68

79
public sealed class AsyncNCalcEngine
810
{
11+
private readonly ConcurrentDictionary<string, Delegate> _functions = new();
12+
13+
public void RegisterFunction(string name, Delegate func) => _functions[name] = func;
14+
915
public async Task<T?> EvalAsync<T>(string expr, object? scope)
1016
{
1117
object? result = await EvalAsync(expr, scope);
@@ -44,6 +50,23 @@ public sealed class AsyncNCalcEngine
4450
{
4551
expression.Parameters[key] = value is Enum e ? Convert.ToInt32(e, CultureInfo.InvariantCulture) : value;
4652
}
53+
expression.EvaluateFunctionAsync += async (name, args) =>
54+
{
55+
if (_functions.TryGetValue(name, out Delegate? func))
56+
{
57+
ParameterInfo[] parameters = func.Method.GetParameters();
58+
object?[] funcArgs = new object[args.Parameters.Length];
59+
for (int i = 0; i < args.Parameters.Length; ++i)
60+
{
61+
object? paramValue = await args.Parameters[i].EvaluateAsync();
62+
funcArgs[i] = CoerceValue(paramValue,
63+
i < parameters.Length ? parameters[i].ParameterType : typeof(object));
64+
}
65+
66+
object? result = func.DynamicInvoke(funcArgs);
67+
args.Result = result;
68+
}
69+
};
4770

4871
return await expression.EvaluateAsync();
4972
}
@@ -100,4 +123,68 @@ public sealed class AsyncNCalcEngine
100123

101124
return finalTarget == typeof(string) ? value.ToString() : value;
102125
}
126+
127+
private object? CoerceValue(object? value, Type targetType)
128+
{
129+
if (value is null)
130+
{
131+
if (targetType.IsValueType)
132+
{
133+
return Activator.CreateInstance(targetType);
134+
}
135+
136+
return value;
137+
}
138+
139+
if (targetType.IsInstanceOfType(value))
140+
{
141+
return value;
142+
}
143+
144+
if (targetType == typeof(string))
145+
{
146+
return value.ToString();
147+
}
148+
149+
if (targetType == typeof(bool))
150+
{
151+
if (value is string sv)
152+
{
153+
if (bool.TryParse(sv, out bool bv))
154+
{
155+
return bv;
156+
}
157+
158+
if (double.TryParse(sv, out double dv))
159+
{
160+
return Math.Abs(dv) > 0.0000001;
161+
}
162+
}
163+
164+
if (value is not IConvertible conv)
165+
{
166+
return false;
167+
}
168+
169+
double dvd = conv.ToDouble(CultureInfo.InvariantCulture);
170+
return Math.Abs(dvd) > 0.0000001;
171+
}
172+
173+
if (!typeof(IConvertible).IsAssignableFrom(targetType))
174+
{
175+
return value;
176+
}
177+
178+
if (value is IConvertible)
179+
{
180+
return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
181+
}
182+
183+
if (value is string s2)
184+
{
185+
return Convert.ChangeType(s2, targetType, CultureInfo.InvariantCulture);
186+
}
187+
188+
return value;
189+
}
103190
}

0 commit comments

Comments
 (0)