diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index cc10d410..1b0b5356 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -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(ITemplateTag templateTag, T value, string condition, CultureInfo? culture); +public delegate string? ValueProvider(ITemplateTag templateTag, T value, string condition, CultureInfo? culture); + +public delegate bool ConditionEvaluator(string? value, CultureInfo? culture); public class ConditionalTagCollection(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive) { @@ -44,10 +45,11 @@ public class ConditionalTagCollection(bool caseSensitive = true) : TagCo /// Register a conditional tag. /// /// - /// A to get the condition's value - public void Add(ITemplateTag templateTag, Conditional conditional) + /// A to get the condition's value + /// + public void Add(ITemplateTag templateTag, ValueProvider 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(bool caseSensitive = true) : TagCo : base(templateTag, conditionExpression) { var tagNameRe = TagNameForRegex(); - NameMatcher = new Regex($"^<(?!)?{tagNameRe}->", options | RegexOptions.Compiled); - NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options | RegexOptions.Compiled); + NameMatcher = new Regex($"^<(?!)?{tagNameRe}->", options); + NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options); CreateConditionExpression = _ => conditionExpression; } - public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, Conditional conditional) + public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider valueProvider, ConditionEvaluator conditionEvaluator) : base(templateTag, Expression.Constant(false)) { // needs to match on at least one character which is not a space @@ -79,17 +81,32 @@ public class ConditionalTagCollection(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 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 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) diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 6447a80f..31e1d9d8 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -146,14 +146,16 @@ public class PropertyTagCollection : 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 (? # - capture inner part as - [^\]]*? # - 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 : TagCollection public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func 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 : TagCollection protected override Expression GetTagExpression(string exactName, Dictionary matchData) { - var formatString = matchData.GetValueOrDefault("format")?.Value ?? ""; + var formatString = Unescape(matchData.GetValueOrDefault("format")) ?? ""; Expression toStringExpression = !ReturnType.IsValueType diff --git a/Source/FileManager/NamingTemplate/TagBase.cs b/Source/FileManager/NamingTemplate/TagBase.cs index cff2ec22..a9d8f1f4 100644 --- a/Source/FileManager/NamingTemplate/TagBase.cs +++ b/Source/FileManager/NamingTemplate/TagBase.cs @@ -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 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}]"; diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index e1257586..6d56a0ba 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -331,7 +331,7 @@ public abstract class Templates private static readonly ConditionalTagCollection combinedConditionalTags = new() { - { TemplateTags.Has, HasValue} + { TemplateTags.Has, TryGetValue, HasValue } }; private static readonly ConditionalTagCollection folderConditionalTags = new() @@ -342,26 +342,29 @@ public abstract class Templates private static readonly List 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 check = (s, _) => !string.IsNullOrWhiteSpace(s); - foreach (var c in allPropertyTags.OfType>()) { 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>()) { 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 diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index bf7105bb..4303ca00 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -83,29 +83,31 @@ public class GetPortionFilename private readonly ConditionalTagCollection _conditional1 = new() { { new TemplateTag { TagName = "ifc1" }, i => i.Condition }, - { new TemplateTag { TagName = "has1" }, HasValue } + { new TemplateTag { TagName = "has1" }, TryGetValue, HasValue } }; private readonly ConditionalTagCollection _conditional2 = new() { { new TemplateTag { TagName = "ifc2" }, i => i.Condition }, - { new TemplateTag { TagName = "has2" }, HasValue } + { new TemplateTag { TagName = "has2" }, TryGetValue, HasValue } }; private readonly ConditionalTagCollection _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() {