Initial version of liquid handler

This commit is contained in:
Sipke Schoorstra 2022-01-12 12:37:40 +01:00
parent 5ecd599996
commit 6e56d83e4d
54 changed files with 518 additions and 50 deletions

View file

@ -78,6 +78,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFram
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Serialization", "src\core\Elsa.Serialization\Elsa.Serialization.csproj", "{55F9B33D-5FAF-4355-97EF-445C93208E85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scripting.Liquid", "src\scripting\Elsa.Scripting.Liquid\Elsa.Scripting.Liquid.csproj", "{A7473430-F228-4534-AEA5-AA8D80E09364}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -164,6 +166,10 @@ Global
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.Build.0 = Release|Any CPU
{A7473430-F228-4534-AEA5-AA8D80E09364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7473430-F228-4534-AEA5-AA8D80E09364}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7473430-F228-4534-AEA5-AA8D80E09364}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7473430-F228-4534-AEA5-AA8D80E09364}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -198,5 +204,6 @@ Global
{D8130816-C1D9-40A3-9079-F2887FE35EA9} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA}
{820016B7-01CD-4032-8239-6A4E53F1A352} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA}
{55F9B33D-5FAF-4355-97EF-445C93208E85} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{A7473430-F228-4534-AEA5-AA8D80E09364} = {2633B8B9-4AEC-4A54-8832-1C940B54E041}
EndGlobalSection
EndGlobal

View file

@ -4,8 +4,6 @@ using Elsa.Management.Options;
using Elsa.Management.Providers;
using Elsa.Management.Serialization;
using Elsa.Management.Services;
using Elsa.Persistence.Mappers;
using Elsa.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Management.Extensions;

View file

@ -26,7 +26,7 @@ export class InputControlSwitch {
@Prop() syntax?: string;
@Prop() expression?: string;
@Prop() defaultSyntax: string = SyntaxNames.Literal;
@Prop() supportedSyntaxes: Array<string> = ['JavaScript']; // TODO: Get available syntaxes from some more centralized settings.
@Prop() supportedSyntaxes: Array<string> = ['JavaScript', 'Liquid']; // TODO: Get available syntaxes from some more centralized settings.
@Prop() isReadOnly?: boolean;
@Prop() codeEditorHeight: string = '10em';
@Prop() codeEditorSingleLineMode: boolean = false;

View file

@ -1,6 +1,5 @@
using Elsa.Contracts;
using Elsa.Models;
using Elsa.Scripting.JavaScript;
using Elsa.Scripting.JavaScript.Expressions;
namespace Elsa.Dsl.Interpreters;

View file

@ -1,7 +1,6 @@
using Elsa.Activities.ControlFlow;
using Elsa.Contracts;
using Elsa.Models;
using Elsa.Scripting.JavaScript;
using Elsa.Scripting.JavaScript.Expressions;
namespace Elsa.Dsl.Interpreters;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Mediator.Contracts;
namespace Elsa.Persistence.Commands;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Mediator.Contracts;
namespace Elsa.Persistence.Commands;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;

View file

@ -1,5 +1,4 @@
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Elsa.Persistence.Entities;
namespace Elsa.Persistence.Commands;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Elsa.Persistence.Entities;
public class WorkflowBookmark : Entity

View file

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using Elsa.Contracts;
namespace Elsa.Persistence.Entities;

View file

@ -1,4 +1,3 @@
using System;
using Elsa.State;
namespace Elsa.Persistence.Entities;

View file

@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Elsa.Persistence.Entities;
using Elsa.Persistence.Models;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Models;
using Elsa.Persistence.Models;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Models;
using Elsa.Persistence.Entities;

View file

@ -1,6 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Contracts;
using Elsa.Mediator.Contracts;
using Elsa.Models;

View file

@ -1,5 +1,3 @@
using System;
namespace Elsa.Persistence.Models;
public record Cursor(string Id, DateTime Sort);

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Elsa.Persistence.Models;
public record PagedList<T>(ICollection<T> Items, int PageSize, int TotalCount);

View file

@ -1,4 +1,3 @@
using System;
using Elsa.Persistence.Entities;
namespace Elsa.Persistence.Models;

View file

@ -1,4 +1,3 @@
using System;
using Elsa.Persistence.Entities;
namespace Elsa.Persistence.Models;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Models;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Models;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Elsa.Persistence.Models;

View file

@ -1,5 +1,4 @@
using System;
using System.ComponentModel;
using System.ComponentModel;
using System.Globalization;
using Elsa.Persistence.Models;

View file

@ -1,7 +1,6 @@
using System.Text.Json;
using Elsa.Contracts;
using Elsa.Management.Serialization;
using Elsa.Management.Serialization.Extensions;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;

View file

@ -1,7 +1,5 @@
using System.Text.Json;
using Elsa.Contracts;
using Elsa.Management.Serialization;
using Elsa.Management.Serialization.Extensions;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;

View file

@ -1,6 +1,5 @@
using System.Text.Json;
using Elsa.Management.Serialization;
using Elsa.Management.Serialization.Extensions;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;
using Elsa.State;

View file

@ -11,6 +11,7 @@
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.EntityFrameworkCore.Sqlite\Elsa.Persistence.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\runtime\Elsa.Runtime.ProtoActor\Elsa.Runtime.ProtoActor.csproj" />
<ProjectReference Include="..\..\..\scripting\Elsa.Scripting.Liquid\Elsa.Scripting.Liquid.csproj" />
</ItemGroup>
</Project>

View file

@ -15,8 +15,8 @@ using Elsa.Pipelines.WorkflowExecution.Components;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Samples.Web1.Activities;
using Elsa.Samples.Web1.Workflows;
using Elsa.Scripting.JavaScript;
using Elsa.Scripting.JavaScript.Extensions;
using Elsa.Scripting.Liquid.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -59,7 +59,9 @@ services
.AddTrigger<HttpTrigger>();
// Register scripting languages.
services.AddJavaScriptExpressions();
services
.AddJavaScriptExpressions()
.AddLiquidExpressions();
// Configure middleware pipeline.
var app = builder.Build();

View file

@ -12,7 +12,6 @@ using Elsa.Dsl.Extensions;
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Persistence.InMemory.Extensions;
using Elsa.Scripting.JavaScript;
using Elsa.Scripting.JavaScript.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

View file

@ -1,5 +1,3 @@
using System;
using System.Threading.Tasks;
using Elsa.Contracts;
namespace Elsa.Scripting.JavaScript.Expressions;

View file

@ -0,0 +1,10 @@
using Fluid;
using Fluid.Values;
namespace Elsa.Scripting.Liquid.Services
{
public interface ILiquidFilter
{
ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext ctx);
}
}

View file

@ -0,0 +1,21 @@
using Elsa.Models;
using Fluid;
namespace Elsa.Scripting.Liquid.Contracts
{
/// <summary>
/// Provides services to render Liquid templates.
/// </summary>
public interface ILiquidTemplateManager
{
/// <summary>
/// Renders a Liquid template as a <see cref="string"/>.
/// </summary>
Task<string?> RenderAsync(string template, ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken = default);
/// <summary>
/// Validates a Liquid template.
/// </summary>
bool Validate(string template, out string error);
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fluid.Core" Version="2.2.13" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Management\Elsa.Management.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Caching.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Caching.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Configuration.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
using Elsa.Contracts;
namespace Elsa.Scripting.Liquid.Expressions;
public class LiquidExpression : IExpression
{
public LiquidExpression(string value) => Value = value;
public string Value { get; }
}

View file

@ -0,0 +1,24 @@
using Elsa.Contracts;
using Elsa.Helpers;
using Elsa.Models;
using Elsa.Scripting.Liquid.Contracts;
using Elsa.Scripting.Liquid.Services;
namespace Elsa.Scripting.Liquid.Expressions;
public class LiquidExpressionHandler : IExpressionHandler
{
private readonly ILiquidTemplateManager _liquidTemplateManager;
public LiquidExpressionHandler(ILiquidTemplateManager liquidTemplateManager)
{
_liquidTemplateManager = liquidTemplateManager;
}
public async ValueTask<object?> EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context)
{
var liquidExpression = (LiquidExpression)expression;
var renderedString = await _liquidTemplateManager.RenderAsync(liquidExpression.Value, context);
return renderedString.ConvertTo(returnType);
}
}

View file

@ -0,0 +1,10 @@
using Elsa.Models;
namespace Elsa.Scripting.Liquid.Expressions;
public class LiquidExpressionReference : RegisterLocationReference
{
public LiquidExpressionReference(LiquidExpression expression) => Expression = expression;
public LiquidExpression Expression { get; }
public override RegisterLocation Declare() => new();
}

View file

@ -0,0 +1,52 @@
using Elsa.Extensions;
using Elsa.Management.Contracts;
using Elsa.Mediator.Extensions;
using Elsa.Scripting.Liquid.Contracts;
using Elsa.Scripting.Liquid.Expressions;
using Elsa.Scripting.Liquid.Filters;
using Elsa.Scripting.Liquid.Handlers;
using Elsa.Scripting.Liquid.Options;
using Elsa.Scripting.Liquid.Providers;
using Elsa.Scripting.Liquid.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scripting.Liquid.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddLiquidExpressions(this IServiceCollection services)
{
return services
.AddMemoryCache()
.AddHandlersFrom<ConfigureLiquidEngine>()
.AddSingleton<IExpressionSyntaxProvider, LiquidExpressionSyntaxProvider>()
.AddSingleton<ILiquidTemplateManager, LiquidTemplateManager>()
.AddSingleton<LiquidParser>()
.AddExpressionHandler<LiquidExpressionHandler, LiquidExpression>()
.AddLiquidFilter<JsonFilter>("json")
.AddLiquidFilter<Base64Filter>("base64");
}
public static IServiceCollection AddLiquidFilter<T>(this IServiceCollection services, string name) where T : class, ILiquidFilter
{
services.Configure<FluidOptions>(options => options.FilterRegistrations[name] = typeof(T));
services.AddScoped<T>();
return services;
}
public static IServiceCollection RegisterLiquidTag(this IServiceCollection services, Action<LiquidParser> configure)
{
services.Configure<FluidOptions>(options => options.ParserConfiguration.Add(configure));
return services;
}
/// <summary>
/// Enables access to .NET configuration via the Configuration keyword.
/// Do not enable this option if you execute user supplied (or otherwise untrusted) workflows.
/// </summary>
public static IServiceCollection EnableLiquidConfigurationAccess(this IServiceCollection services)
{
services.Configure<FluidOptions>(options => options.AllowConfigurationAccess = true);
return services;
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Scripting.Liquid.Options;
using Elsa.Scripting.Liquid.Services;
using Fluid;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scripting.Liquid.Extensions
{
public static class TemplateContextExtensions
{
internal static void AddFilters(this TemplateContext templateContext, FluidOptions options, IServiceProvider services)
{
foreach (var (key, value) in options.FilterRegistrations)
{
templateContext.Options.Filters.AddFilter(key, (input, arguments, ctx) =>
{
var filter = (ILiquidFilter)services.GetRequiredService(value);
return filter.ProcessAsync(input, arguments, ctx);
});
}
}
}
}

View file

@ -0,0 +1,39 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using Elsa.Scripting.Liquid.Services;
using Fluid;
using Fluid.Values;
namespace Elsa.Scripting.Liquid.Filters
{
public class Base64Filter : ILiquidFilter
{
public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context)
{
var text = InputToString(input, context);
if (text == null)
return new ValueTask<FluidValue>(NilValue.Instance);
var bytes = Encoding.UTF8.GetBytes(text);
var base64 = Convert.ToBase64String(bytes);
return new ValueTask<FluidValue>(new StringValue(base64));
}
private string? InputToString(FluidValue input, TemplateContext context)
{
return input.Type switch
{
FluidValues.Array => JsonSerializer.Serialize(input.Enumerate(context).Select(o => o.ToObjectValue())),
FluidValues.Boolean => input.ToBooleanValue().ToString(),
FluidValues.Nil => null,
FluidValues.Number => input.ToNumberValue().ToString(CultureInfo.InvariantCulture),
FluidValues.DateTime or FluidValues.Dictionary or FluidValues.Object => JsonSerializer.Serialize(input.ToObjectValue()),
FluidValues.String => input.ToStringValue(),
_ => throw new ArgumentOutOfRangeException(nameof(input), "Unrecognized FluidValue")
};
}
}
}

View file

@ -0,0 +1,42 @@
using System.Text.Json;
using Elsa.Scripting.Liquid.Services;
using Fluid;
using Fluid.Values;
namespace Elsa.Scripting.Liquid.Filters
{
public class JsonFilter : ILiquidFilter
{
public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context)
{
switch (input.Type)
{
case FluidValues.Array:
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.Enumerate(context).Select(o => o.ToObjectValue()))));
case FluidValues.Boolean:
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToBooleanValue())));
case FluidValues.Nil:
return new ValueTask<FluidValue>(FluidValue.Create("null", context.Options));
case FluidValues.Number:
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToNumberValue())));
case FluidValues.DateTime:
case FluidValues.Dictionary:
case FluidValues.Object:
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToObjectValue())));
case FluidValues.String:
var stringValue = input.ToStringValue();
return string.IsNullOrWhiteSpace(stringValue)
? new ValueTask<FluidValue>(input)
: new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(stringValue)));
}
throw new NotSupportedException("Unrecognized FluidValue");
}
}
}

View file

@ -0,0 +1,47 @@
using System.Dynamic;
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Elsa.Scripting.Liquid.Helpers;
using Elsa.Scripting.Liquid.Notifications;
using Elsa.Scripting.Liquid.Options;
using Fluid;
using Fluid.Values;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Elsa.Scripting.Liquid.Handlers
{
public class ConfigureLiquidEngine : INotificationHandler<RenderingLiquidTemplate>
{
private readonly IConfiguration _configuration;
private readonly FluidOptions _fluidOptions;
public ConfigureLiquidEngine(IConfiguration configuration, IOptions<FluidOptions> fluidOptions)
{
_configuration = configuration;
_fluidOptions = fluidOptions.Value;
}
public Task HandleAsync(RenderingLiquidTemplate notification, CancellationToken cancellationToken)
{
var context = notification.TemplateContext;
var options = context.Options;
var memberAccessStrategy = options.MemberAccessStrategy;
memberAccessStrategy.Register<ExpandoObject>();
memberAccessStrategy.Register<LiquidPropertyAccessor, FluidValue>((x, name) => x.GetValueAsync(name));
memberAccessStrategy.Register<ExpandoObject, object>((x, name) => ((IDictionary<string, object>)x!)[name]);
if (_fluidOptions.AllowConfigurationAccess)
{
memberAccessStrategy.Register<ActivityExecutionContext, LiquidPropertyAccessor>("Configuration", x => new LiquidPropertyAccessor(name => ToFluidValue(GetConfigurationValue(name), options)!));
memberAccessStrategy.Register<ConfigurationSectionWrapper, ConfigurationSectionWrapper?>((source, name) => source.GetSection(name));
}
return Task.CompletedTask;
}
private ConfigurationSectionWrapper GetConfigurationValue(string name) => new(_configuration.GetSection(name));
private Task<FluidValue> ToFluidValue(object? input, TemplateOptions options) => Task.FromResult(FluidValue.Create(input, options));
}
}

View file

@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
namespace Elsa.Scripting.Liquid.Helpers
{
public class ConfigurationSectionWrapper
{
private readonly IConfigurationSection _section;
public ConfigurationSectionWrapper(IConfigurationSection section)
{
_section = section;
}
public override string ToString() => _section.Value;
public ConfigurationSectionWrapper GetSection(string name) => new(_section.GetSection(name));
}
}

View file

@ -0,0 +1,8 @@
using Elsa.Models;
namespace Elsa.Scripting.Liquid.Helpers
{
public record LiquidActivityModel(ActivityExecutionContext ActivityExecutionContext, string? ActivityName, string? ActivityId)
{
}
}

View file

@ -0,0 +1,12 @@
namespace Elsa.Scripting.Liquid.Helpers
{
/// <summary>
/// Can be used to provide a factory to return an object based on a property name that is unknown at registration time.
/// </summary>
public class LiquidObjectAccessor<T>
{
private readonly Func<string, Task<T>> _getter;
public LiquidObjectAccessor(Func<string, Task<T>> getter) => _getter = getter;
public Task<T> GetValueAsync(string identifier) => _getter(identifier);
}
}

View file

@ -0,0 +1,17 @@
using Fluid.Values;
namespace Elsa.Scripting.Liquid.Helpers
{
/// <summary>
/// Can be used to provide a factory to return a value based on a property name
/// that is unknown at registration time.
///
/// e.g. {{ LiquidPropertyAccessor.MyPropertyName }} (MyPropertyName will be passed as the identifier argument to the factory)
/// </summary>
public class LiquidPropertyAccessor : LiquidObjectAccessor<FluidValue>
{
public LiquidPropertyAccessor(Func<string, Task<FluidValue>> getter) : base(getter!)
{
}
}
}

View file

@ -0,0 +1,7 @@
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Fluid;
namespace Elsa.Scripting.Liquid.Notifications;
public record RenderingLiquidTemplate(TemplateContext TemplateContext, ExpressionExecutionContext Context) : INotification;

View file

@ -0,0 +1,11 @@
using Elsa.Scripting.Liquid.Services;
namespace Elsa.Scripting.Liquid.Options
{
public class FluidOptions
{
public Dictionary<string, Type> FilterRegistrations { get; } = new();
public IList<Action<LiquidParser>> ParserConfiguration { get; } = new List<Action<LiquidParser>>();
public bool AllowConfigurationAccess { get; set; }
}
}

View file

@ -0,0 +1,49 @@
using Elsa.Contracts;
using Elsa.Management.Contracts;
using Elsa.Management.Models;
using Elsa.Scripting.Liquid.Expressions;
namespace Elsa.Scripting.Liquid.Providers;
public class LiquidExpressionSyntaxProvider : IExpressionSyntaxProvider
{
public const string SyntaxName = "Liquid";
private readonly IIdentityGenerator _identityGenerator;
public LiquidExpressionSyntaxProvider(IIdentityGenerator identityGenerator) => _identityGenerator = identityGenerator;
public ValueTask<IEnumerable<ExpressionSyntaxDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var liquidDescriptor = CreateLiquidDescriptor();
return ValueTask.FromResult<IEnumerable<ExpressionSyntaxDescriptor>>(new[] { liquidDescriptor });
}
private ExpressionSyntaxDescriptor CreateLiquidDescriptor() => new()
{
Syntax = SyntaxName,
Type = typeof(LiquidExpression),
CreateExpression = CreateJavaScriptExpression,
CreateLocationReference = context =>
{
var reference = new LiquidExpressionReference(context.GetExpression<LiquidExpression>());
if (string.IsNullOrWhiteSpace(reference.Id))
reference.Id = GenerateId();
return reference;
},
CreateSerializableObject = context => new
{
Type = SyntaxName,
context.GetExpression<LiquidExpression>().Value
}
};
private IExpression CreateJavaScriptExpression(ExpressionConstructorContext context)
{
var script = context.Element.TryGetProperty("value", out var p) ? p.ToString() : "";
return new LiquidExpression(script);
}
private string GenerateId() => _identityGenerator.GenerateId();
}

View file

@ -0,0 +1,15 @@
using Elsa.Scripting.Liquid.Options;
using Fluid;
using Microsoft.Extensions.Options;
namespace Elsa.Scripting.Liquid.Services
{
public class LiquidParser : FluidParser
{
public LiquidParser(IOptions<FluidOptions> options)
{
foreach (var configuration in options.Value.ParserConfiguration)
configuration(this);
}
}
}

View file

@ -0,0 +1,64 @@
using System.Text.Encodings.Web;
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Elsa.Scripting.Liquid.Contracts;
using Elsa.Scripting.Liquid.Notifications;
using Fluid;
using Microsoft.Extensions.Caching.Memory;
namespace Elsa.Scripting.Liquid.Services
{
public class LiquidTemplateManager : ILiquidTemplateManager
{
private readonly LiquidParser _parser;
private readonly IMemoryCache _memoryCache;
private readonly IPublisher _publisher;
public LiquidTemplateManager(LiquidParser parser, IMemoryCache memoryCache, IPublisher publisher)
{
_parser = parser;
_memoryCache = memoryCache;
_publisher = publisher;
}
public async Task<string?> RenderAsync(string template, ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(template))
return default!;
var result = GetCachedTemplate(template);
var templateContext = await CreateTemplateContextAsync(expressionExecutionContext, cancellationToken);
return await result.RenderAsync(templateContext, HtmlEncoder.Default);
}
private IFluidTemplate GetCachedTemplate(string source)
{
var result = _memoryCache.GetOrCreate(
source,
e =>
{
if (!TryParse(source, out var parsed, out var error))
TryParse(error, out parsed, out error);
// TODO: add signal based cache invalidation.
e.SetSlidingExpiration(TimeSpan.FromSeconds(30));
return parsed;
});
return result;
}
public bool Validate(string template, out string error) => TryParse(template, out _, out error);
private bool TryParse(string template, out IFluidTemplate result, out string error) => _parser.TryParse(template, out result, out error);
private async Task<TemplateContext> CreateTemplateContextAsync(ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken)
{
var context = new TemplateContext(expressionExecutionContext, new TemplateOptions());
await _publisher.PublishAsync(new RenderingLiquidTemplate(context, expressionExecutionContext), cancellationToken);
context.SetValue("ExpressionExecutionContext", expressionExecutionContext);
return context;
}
}
}