Files
Libation/Source/FileManager/NamingTemplate/NamingTemplate.cs
2026-03-22 13:55:24 +01:00

296 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
namespace FileManager.NamingTemplate;
public class NamingTemplate
{
public string TemplateText { get; private set; } = string.Empty;
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
public IEnumerable<ITemplateTag> TagsRegistered => _tagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
public IEnumerable<string> Warnings => _errors.Concat(_warnings);
public IEnumerable<string> Errors => _errors;
private Delegate? _templateToString;
private readonly List<string> _warnings = [];
private readonly List<string> _errors = [];
private readonly IEnumerable<TagCollection> _tagCollections;
private readonly List<ITemplateTag> _tagsInUse = [];
public const string ErrorNullIsInvalid = "Null template is invalid.";
public const string WarningEmpty = "Template is empty.";
public const string WarningWhiteSpace = "Template is white space.";
public const string WarningNoTags = "Should use tags. Eg: <title>";
/// <summary>
/// Invoke the <see cref="NamingTemplate"/>
/// </summary>
/// <param name="culture"></param>
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
public TemplatePart Evaluate(params object?[] propertyClasses) //CultureInfo? culture,
{
if (_templateToString is null)
throw new InvalidOperationException();
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
// First parameter is "this", so ignore it.
var parameters = _templateToString.Method.GetParameters();
int skip = _templateToString.Target == null ? 0 : 1;
var delegateArgTypes = parameters.Skip(skip).ToList();
object?[] args = new object?[delegateArgTypes.Count];
// args = delegateArgTypes.Join(propertyClasses, dat => dat.ParameterType, pc => pc?.GetType(), (_, i) => i,
// EqualityComparer<Type?>.Create((datType, pcType) => datType!.IsAssignableFrom(pcType))).ToArray();
for (int i = 0; i < delegateArgTypes.Count; i++)
{
var p = delegateArgTypes[i];
if (typeof(CultureInfo).IsAssignableFrom(p.ParameterType) && false)
{
args[i] = null;//culture;
}
else
{
args[i] = propertyClasses.FirstOrDefault(pc => pc != null && p.ParameterType.IsInstanceOfType(pc));
}
}
if (args.Length != delegateArgTypes.Count)
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
return (_templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
}
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
/// <param name="template">The template string to parse</param>
/// <param name="tagCollections">A collection of <see cref="TagCollection"/> with
/// properties registered to match to the <paramref name="template"/></param>
public static NamingTemplate Parse(string? template, IEnumerable<TagCollection> tagCollections)
{
var namingTemplate = new NamingTemplate(tagCollections);
try
{
BinaryNode intermediate = namingTemplate.IntermediateParse(template);
Expression evalTree = GetExpressionTree(intermediate);
namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile();
}
catch (Exception ex)
{
namingTemplate._errors.Add(ex.Message);
}
return namingTemplate;
}
private NamingTemplate(IEnumerable<TagCollection> properties)
{
_tagCollections = properties;
}
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
private static Expression GetExpressionTree(BinaryNode? node)
{
if (node is null) return TemplatePart.Blank;
if (node.IsValue) return node.Expression;
if (node.IsConditional) return Expression.Condition(node.Expression, ConcatExpression(node), TemplatePart.Blank);
return ConcatExpression(node);
static Expression ConcatExpression(BinaryNode node)
=> TemplatePart.CreateConcatenation(GetExpressionTree(node.LeftChild), GetExpressionTree(node.RightChild));
}
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
private BinaryNode IntermediateParse(string? templateString)
{
if (templateString is null)
throw new ArgumentException(ErrorNullIsInvalid);
if (string.IsNullOrEmpty(templateString))
_warnings.Add(WarningEmpty);
else if (string.IsNullOrWhiteSpace(templateString))
_warnings.Add(WarningWhiteSpace);
TemplateText = templateString;
BinaryNode topNode = BinaryNode.CreateRoot();
BinaryNode? currentNode = topNode;
List<char> literalChars = new();
while (templateString.Length > 0)
{
if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
{
CheckAndAddLiterals();
if (propertyTag is IClosingPropertyTag)
currentNode = currentNode.AddNewNode(BinaryNode.CreateConditional(propertyTag.TemplateTag, valueExpression));
else
{
currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(propertyTag.TemplateTag, valueExpression));
_tagsInUse.Add(propertyTag.TemplateTag);
}
templateString = templateString[exactPropertyName.Length..];
}
else if (StartsWithClosing(templateString, out exactPropertyName, out var closingPropertyTag))
{
CheckAndAddLiterals();
BinaryNode? lastParenth = currentNode;
while (lastParenth?.IsConditional is false)
lastParenth = lastParenth.Parent;
if (lastParenth?.Parent is null)
{
_warnings.Add($"Missing <{closingPropertyTag.TemplateTag.TagName}-> open conditional.");
break;
}
else if (lastParenth.Name != closingPropertyTag.TemplateTag.TagName)
{
_warnings.Add($"Missing <-{lastParenth.Name}> closing conditional.");
break;
}
currentNode = lastParenth.Parent;
templateString = templateString[exactPropertyName.Length..];
}
else
{
//templateString does not start with a tag, so the first
//character is a literal and not part of a tag expression.
literalChars.Add(templateString[0]);
templateString = templateString[1..];
}
}
CheckAndAddLiterals();
//Check for any conditionals that haven't been closed
while (currentNode is not null)
{
if (currentNode.IsConditional)
_warnings.Add($"Missing <-{currentNode.Name}> closing conditional.");
currentNode = currentNode.Parent;
}
if (!_tagsInUse.Any())
_warnings.Add(WarningNoTags);
return topNode;
void CheckAndAddLiterals()
{
if (literalChars.Count != 0)
{
currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(string.Concat(literalChars)));
literalChars.Clear();
}
}
}
private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
{
foreach (var pc in _tagCollections)
{
if (pc.StartsWith(template, out exactName, out propertyTag, out valueExpression))
return true;
}
exactName = null;
valueExpression = null;
propertyTag = null;
return false;
}
private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
{
foreach (var pc in _tagCollections)
{
if (pc.StartsWithClosing(template, out exactName, out closingPropertyTag))
return true;
}
exactName = null;
closingPropertyTag = null;
return false;
}
private class BinaryNode
{
public string Name { get; }
public BinaryNode? Parent { get; private set; }
public BinaryNode? RightChild { get; private set; }
public BinaryNode? LeftChild { get; private set; }
public Expression Expression { get; }
public bool IsConditional { get; private init; }
public bool IsValue { get; private init; }
public static BinaryNode CreateRoot() => new("Root", Expression.Empty());
public static BinaryNode CreateValue(string literal)
=> new("Literal", TemplatePart.CreateLiteral(literal))
{
IsValue = true
};
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property)
=> new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property))
{
IsValue = true
};
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property)
=> new(templateTag.TagName, property)
{
IsConditional = true
};
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
{
var newNode = new BinaryNode("Concatenation", Expression.Empty())
{
LeftChild = left,
RightChild = right
};
newNode.LeftChild.Parent = newNode;
newNode.RightChild.Parent = newNode;
return newNode;
}
private BinaryNode(string name, Expression expression)
{
Name = name;
Expression = expression;
}
public override string ToString() => Name;
public BinaryNode AddNewNode(BinaryNode newNode)
{
BinaryNode currentNode = this;
if (LeftChild is null)
{
newNode.Parent = currentNode;
LeftChild = newNode;
}
else if (RightChild is null)
{
newNode.Parent = currentNode;
RightChild = newNode;
}
else
{
RightChild = CreateConcatenation(RightChild, newNode);
RightChild.Parent = currentNode;
currentNode = RightChild;
}
return newNode.IsConditional ? newNode : currentNode;
}
}
}