Formats are now escapable and the evaluation of HasValue has been transformed into a configuration parameter.

This commit is contained in:
Jo-Be-Co
2026-03-14 00:46:37 +01:00
parent 1d6d4ff9ac
commit d8ce7cc9b0
5 changed files with 92 additions and 42 deletions

View File

@@ -1,5 +1,4 @@

using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -24,7 +23,9 @@ internal interface IClosingPropertyTag : IPropertyTag
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
}
public delegate bool Conditional<T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
public delegate string? ValueProvider<in T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
public delegate bool ConditionEvaluator(string? value, CultureInfo? culture);
public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
{
@@ -44,10 +45,11 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
/// Register a conditional tag.
/// </summary>
/// <param name="templateTag"></param>
/// <param name="conditional">A <see cref="Conditional{TClass}"/> to get the condition's <see cref="bool"/> value</param>
public void Add(ITemplateTag templateTag, Conditional<TClass> conditional)
/// <param name="valueProvider">A <see cref="ValueProvider{T}"/> to get the condition's <see cref="bool"/> value</param>
/// <param name="conditionEvaluator"></param>
public void Add(ITemplateTag templateTag, ValueProvider<TClass> valueProvider, ConditionEvaluator conditionEvaluator)
{
AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, conditional));
AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, valueProvider, conditionEvaluator));
}
private class ConditionalTag : TagBase, IClosingPropertyTag
@@ -61,12 +63,12 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
: base(templateTag, conditionExpression)
{
var tagNameRe = TagNameForRegex();
NameMatcher = new Regex($"^<(?<not>!)?{tagNameRe}->", options | RegexOptions.Compiled);
NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options | RegexOptions.Compiled);
NameMatcher = new Regex($"^<(?<not>!)?{tagNameRe}->", options);
NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options);
CreateConditionExpression = _ => conditionExpression;
}
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, Conditional<TClass> conditional)
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider<TClass> valueProvider, ConditionEvaluator conditionEvaluator)
: base(templateTag, Expression.Constant(false))
{
// <property> needs to match on at least one character which is not a space
@@ -79,17 +81,32 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
)? # end of optional property and check part
\s*-> # Opening tags end with '->' and closing tags begin with '<-', so both sides visually point toward each other
"""
, options | RegexOptions.Compiled);
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options | RegexOptions.Compiled);
, options);
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
CreateConditionExpression = condition
=> Expression.Call(
conditional.Target is null ? null : Expression.Constant(conditional.Target),
conditional.Method,
Expression.Constant(templateTag),
parameter,
Expression.Constant(condition),
CultureParameter);
CreateConditionExpression = property
=> ConditionEvaluatorCall(templateTag, parameter, valueProvider, property, conditionEvaluator);
}
private static MethodCallExpression ConditionEvaluatorCall(ITemplateTag templateTag, ParameterExpression parameter, ValueProvider<TClass> valueProvider, string? property,
ConditionEvaluator conditionEvaluator)
{
return Expression.Call(
conditionEvaluator.Target is null ? null : Expression.Constant(conditionEvaluator.Target),
conditionEvaluator.Method,
ValueProviderCall(templateTag, parameter, valueProvider, property),
CultureParameter);
}
private static MethodCallExpression ValueProviderCall(ITemplateTag templateTag, ParameterExpression parameter, ValueProvider<TClass> valueProvider, string? property)
{
return Expression.Call(
valueProvider.Target is null ? null : Expression.Constant(valueProvider.Target),
valueProvider.Method,
Expression.Constant(templateTag),
parameter,
Expression.Constant(property),
CultureParameter);
}
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)

View File

@@ -146,14 +146,16 @@ public class PropertyTagCollection<TClass> : TagCollection
^< # tags start with a '<'
{TagNameForRegex()} # next the tagname needs to be matched with space being made optional. Also escape all '#'
(?:\s* # optional whitespace
\[ # optional format details enclosed in '[' and ']'.
\[\s* # optional format details enclosed in '[' and ']'. Format shall be trimmed. So match whitespace first
(?<format> # - capture inner part as <format>
[^\]]*? # - match any character except ']'
) #
(?:\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|[^\\\]])*? # - match any character except '\' and ']' non greedy so the match won't end whith whitespace
)\s* # - the whitespace after the format is optional
\] # - closing the format part
)?\s*> # Tags end with '>'
"""
, options | RegexOptions.Compiled);
, options);
CreateToStringExpression = (expVal, format) =>
Expression.Call(
formatter.Target is null ? null : Expression.Constant(formatter.Target),
@@ -167,7 +169,7 @@ public class PropertyTagCollection<TClass> : TagCollection
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString)
: base(templateTag, propertyGetter)
{
NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options | RegexOptions.Compiled);
NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options);
CreateToStringExpression = (expVal, _) =>
Expression.Call(
toString.Target is null ? null : Expression.Constant(toString.Target),
@@ -177,7 +179,7 @@ public class PropertyTagCollection<TClass> : TagCollection
protected override Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData)
{
var formatString = matchData.GetValueOrDefault("format")?.Value ?? "";
var formatString = Unescape(matchData.GetValueOrDefault("format")) ?? "";
Expression toStringExpression
= !ReturnType.IsValueType

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
namespace FileManager.NamingTemplate;
@@ -66,6 +67,31 @@ internal abstract class TagBase : IPropertyTag
return TemplateTag.TagName.Replace(" ", @"\s*").Replace("#", @"\#");
}
protected static string? Unescape(Group? group)
{
return group?.Success ?? false ? Unescape(group.ValueSpan) : null;
}
protected static string Unescape(ReadOnlySpan<char> valueSpan)
{
if (valueSpan.IsEmpty) return "";
var first = valueSpan.IndexOf('\\');
if (first < 0)
return valueSpan.ToString();
var sb = new StringBuilder(valueSpan.Length);
sb.Append(valueSpan[..first]);
for (var i = first; i < valueSpan.Length; i++)
{
if (valueSpan[i] == '\\' && i + 1 < valueSpan.Length)
i++; // skip backslash and take the next char
sb.Append(valueSpan[i]);
}
return sb.ToString();
}
public override string ToString()
{
return $"[Name = {TemplateTag.TagName}, Type = {ReturnType.Name}]";

View File

@@ -331,7 +331,7 @@ public abstract class Templates
private static readonly ConditionalTagCollection<CombinedDto> combinedConditionalTags = new()
{
{ TemplateTags.Has, HasValue}
{ TemplateTags.Has, TryGetValue, HasValue }
};
private static readonly ConditionalTagCollection<LibraryBookDto> folderConditionalTags = new()
@@ -342,26 +342,29 @@ public abstract class Templates
private static readonly List<TagCollection> allPropertyTags =
chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).ToList();
private static bool HasValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture)
private static string? TryGetValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture)
{
Func<string?, CultureInfo?, bool> check = (s, _) => !string.IsNullOrWhiteSpace(s);
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<LibraryBookDto>>())
{
if (c.TryGetValue(property, dtos.LibraryBook, culture, out var value))
return check(value, culture ?? CultureInfo.CurrentCulture);
return value;
}
if (dtos.MultiConvert is null)
return false;
return null;
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<MultiConvertFileProperties>>())
{
if (c.TryGetValue(property, dtos.MultiConvert, culture, out var value))
return check(value, culture ?? CultureInfo.CurrentCulture);
return value;
}
return false;
return null;
}
private static bool HasValue(string? value, CultureInfo? culture)
{
return !string.IsNullOrWhiteSpace(value);
}
#endregion

View File

@@ -83,29 +83,31 @@ public class GetPortionFilename
private readonly ConditionalTagCollection<PropertyClass1> _conditional1 = new()
{
{ new TemplateTag { TagName = "ifc1" }, i => i.Condition },
{ new TemplateTag { TagName = "has1" }, HasValue }
{ new TemplateTag { TagName = "has1" }, TryGetValue, HasValue }
};
private readonly ConditionalTagCollection<PropertyClass2> _conditional2 = new()
{
{ new TemplateTag { TagName = "ifc2" }, i => i.Condition },
{ new TemplateTag { TagName = "has2" }, HasValue }
{ new TemplateTag { TagName = "has2" }, TryGetValue, HasValue }
};
private readonly ConditionalTagCollection<PropertyClass3> _conditional3 = new()
{
{ new TemplateTag { TagName = "ifc3" }, i => i.Condition },
{ new TemplateTag { TagName = "has3" }, HasValue }
{ new TemplateTag { TagName = "has3" }, TryGetValue, HasValue }
};
private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture)
=> props1.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture)
=> props1.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture)
=> props2.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture)
=> props2.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture)
=> props3.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture)
=> props3.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
private static bool HasValue(string? value, CultureInfo? culture) => !string.IsNullOrWhiteSpace(value);
private readonly PropertyClass1 _propertyClass1 = new()
{