mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
Allow two step formatting to get checkable values and not only single stringa
Introduce <is-> Tag. Like <has-> but with additional check on content. Retrieve objects instead of string for conditions Pass undefined formats as null instead of empty strings
This commit is contained in:
@@ -9,12 +9,29 @@ public static partial class CommonFormatters
|
||||
{
|
||||
public const string DefaultDateFormat = "yyyy-MM-dd";
|
||||
|
||||
public delegate string PropertyFormatter<in T>(ITemplateTag templateTag, T? value, string formatString, CultureInfo? culture);
|
||||
public delegate TFormatted? PropertyFormatter<in TProperty, out TFormatted>(ITemplateTag templateTag, TProperty? value, string? formatString, CultureInfo? culture);
|
||||
|
||||
public static string StringFormatter(ITemplateTag _, string? value, string formatString, CultureInfo? culture)
|
||||
public delegate string? PropertyFinalizer<in T>(ITemplateTag templateTag, T? value, CultureInfo? culture);
|
||||
|
||||
public static PropertyFinalizer<TProperty> ToPropertyFormatter<TProperty, TPreFormatted>(PropertyFormatter<TProperty, TPreFormatted> preFormatter,
|
||||
PropertyFinalizer<TPreFormatted> finalizer)
|
||||
{
|
||||
return (templateTag, value, culture) => finalizer(templateTag, preFormatter(templateTag, value, null, culture), culture);
|
||||
}
|
||||
|
||||
public static PropertyFinalizer<TPropertyValue> ToFinalizer<TPropertyValue>(PropertyFormatter<TPropertyValue, string> formatter)
|
||||
{
|
||||
return (templateTag, value, culture) => formatter(templateTag, value, null, culture);
|
||||
}
|
||||
|
||||
public static string? StringFinalizer(ITemplateTag templateTag, string? value, CultureInfo? culture) => value ?? "";
|
||||
|
||||
public static TPropertyValue? IdlePreFormatter<TPropertyValue>(ITemplateTag templateTag, TPropertyValue? value, string? formatString, CultureInfo? culture) => value;
|
||||
|
||||
public static string StringFormatter(ITemplateTag _, string? value, string? formatString, CultureInfo? culture)
|
||||
=> _StringFormatter(value, formatString, culture);
|
||||
|
||||
private static string _StringFormatter(string? value, string formatString, CultureInfo? culture)
|
||||
private static string _StringFormatter(string? value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
if (string.IsNullOrEmpty(formatString)) return value;
|
||||
@@ -47,7 +64,7 @@ public static partial class CommonFormatters
|
||||
|
||||
// is this function is called from toString implementation of the IFormattable interface, we only get a IFormatProvider
|
||||
var culture = provider as CultureInfo ?? (provider?.GetFormat(typeof(CultureInfo)) as CultureInfo);
|
||||
return TagFormatRegex().Replace(templateString, GetValueForMatchingTag);
|
||||
return CollapseSpacesAndTrimRegex().Replace(TagFormatRegex().Replace(templateString, GetValueForMatchingTag), "");
|
||||
|
||||
string GetValueForMatchingTag(Match m)
|
||||
{
|
||||
@@ -64,6 +81,10 @@ public static partial class CommonFormatters
|
||||
}
|
||||
}
|
||||
|
||||
// Matches runs of spaces followed by a space as well as runs of spaces at the beginning or the end of a string (does NOT touch tabs/newlines).
|
||||
[GeneratedRegex(@"^ +| +(?=$| )")]
|
||||
private static partial Regex CollapseSpacesAndTrimRegex();
|
||||
|
||||
// The templateString is scanned for contained braces with an enclosed tagname.
|
||||
// The tagname may be followed by an optional format specifier separated by a colon.
|
||||
// All other parts of the template string are left untouched as well as the braces where the tagname is unknown.
|
||||
@@ -71,13 +92,13 @@ public static partial class CommonFormatters
|
||||
[GeneratedRegex(@"\{(?<tag>[[A-Z]+|#)(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex TagFormatRegex();
|
||||
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString, CultureInfo? culture)
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string? formatString, CultureInfo? culture)
|
||||
=> value?.ToString(formatString, culture) ?? "";
|
||||
|
||||
public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString, CultureInfo? culture)
|
||||
public static string IntegerFormatter(ITemplateTag templateTag, int value, string? formatString, CultureInfo? culture)
|
||||
=> FloatFormatter(templateTag, value, formatString, culture);
|
||||
|
||||
public static string FloatFormatter(ITemplateTag _, float value, string formatString, CultureInfo? culture)
|
||||
public static string FloatFormatter(ITemplateTag _, float value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (!int.TryParse(formatString, out var numDigits) || numDigits <= 0) return value.ToString(formatString, culture);
|
||||
@@ -89,7 +110,7 @@ public static partial class CommonFormatters
|
||||
return new string('0', zeroPad) + strValue;
|
||||
}
|
||||
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString, CultureInfo? culture)
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.InvariantCulture;
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
@@ -97,7 +118,7 @@ public static partial class CommonFormatters
|
||||
return value.ToString(formatString, culture);
|
||||
}
|
||||
|
||||
public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string formatString, CultureInfo? culture)
|
||||
public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
return StringFormatter(templateTag, language?.Trim(), "3u", culture);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -23,11 +24,11 @@ internal interface IClosingPropertyTag : IPropertyTag
|
||||
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
|
||||
}
|
||||
|
||||
public delegate string? ValueProvider<in T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
|
||||
public delegate object? ValueProvider<in T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
|
||||
|
||||
public delegate bool ConditionEvaluator(string? value, CultureInfo? culture);
|
||||
public delegate bool ConditionEvaluator(object? value, CultureInfo? culture);
|
||||
|
||||
public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
|
||||
public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a conditional tag.
|
||||
@@ -45,19 +46,29 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
/// Register a conditional tag.
|
||||
/// </summary>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="valueProvider">A <see cref="ValueProvider{T}"/> to get the condition's <see cref="bool"/> value</param>
|
||||
/// <param name="conditionEvaluator"></param>
|
||||
/// <param name="valueProvider">A <see cref="ValueProvider{T}"/> to get the condition's value</param>
|
||||
/// <param name="conditionEvaluator">A <see cref="ConditionEvaluator"/> to evaluate the condition's value</param>
|
||||
public void Add(ITemplateTag templateTag, ValueProvider<TClass> valueProvider, ConditionEvaluator conditionEvaluator)
|
||||
{
|
||||
AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, valueProvider, conditionEvaluator));
|
||||
}
|
||||
|
||||
private class ConditionalTag : TagBase, IClosingPropertyTag
|
||||
/// <summary>
|
||||
/// Register a conditional tag.
|
||||
/// </summary>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="valueProvider">A <see cref="ValueProvider{T}"/> to get the condition's value. The value will be evaluated by a check specified by the tag itself.</param>
|
||||
public void Add(ITemplateTag templateTag, ValueProvider<TClass> valueProvider)
|
||||
{
|
||||
AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, valueProvider));
|
||||
}
|
||||
|
||||
private partial class ConditionalTag : TagBase, IClosingPropertyTag
|
||||
{
|
||||
public override Regex NameMatcher { get; }
|
||||
public Regex NameCloseMatcher { get; }
|
||||
|
||||
private Func<string?, Expression> CreateConditionExpression { get; }
|
||||
private Func<string?, string?, Expression> CreateConditionExpression { get; }
|
||||
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
|
||||
: base(templateTag, conditionExpression)
|
||||
@@ -65,7 +76,8 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
var tagNameRe = TagNameForRegex();
|
||||
NameMatcher = new Regex($"^<(?<not>!)?{tagNameRe}->", options);
|
||||
NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options);
|
||||
CreateConditionExpression = _ => conditionExpression;
|
||||
|
||||
CreateConditionExpression = (_, _) => conditionExpression;
|
||||
}
|
||||
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider<TClass> valueProvider, ConditionEvaluator conditionEvaluator)
|
||||
@@ -84,10 +96,41 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
, options);
|
||||
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
||||
|
||||
CreateConditionExpression = property
|
||||
CreateConditionExpression = (property, _)
|
||||
=> ConditionEvaluatorCall(templateTag, parameter, valueProvider, property, conditionEvaluator);
|
||||
}
|
||||
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider<TClass> valueProvider)
|
||||
: base(templateTag, Expression.Constant(false))
|
||||
{
|
||||
// <property> needs to match on at least one character which is not a space
|
||||
// though we will capture check enclosed in [] at the end of the tag the property itself migth also have a [] part for formatting purposes
|
||||
NameMatcher = new Regex($"""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^<(?<not>!)? # tags start with a '<'. Condtionals allow an optional ! captured in <not> to negate the condition
|
||||
{TagNameForRegex()} # next the tagname needs to be matched with space being made optional. Also escape all '#'
|
||||
(?:\s+ # the following part is optional. If present it starts with some whitespace
|
||||
(?<property>.+? # - capture the <property> non greedy so it won't end on whitespace, '[' or '-' (if match is possible)
|
||||
(?<!\s)) # - don't let <property> end with a whitepace. Otherwise "<tagname [foobar]->" would be matchable.
|
||||
(?:\s*\[\s* # optional check details enclosed in '[' and ']'. Check shall be trimmed. So match whitespace first
|
||||
(?<check> # - capture inner part as <check>
|
||||
(?:\\. # - '\' 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 check is optional
|
||||
\])? # - closing the check part
|
||||
)? # 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);
|
||||
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
||||
|
||||
CreateConditionExpression = (property, checkString) =>
|
||||
{
|
||||
var conditionEvaluator = GetPredicate(checkString);
|
||||
return ConditionEvaluatorCall(templateTag, parameter, valueProvider, property, conditionEvaluator);
|
||||
};
|
||||
}
|
||||
|
||||
private static MethodCallExpression ConditionEvaluatorCall(ITemplateTag templateTag, ParameterExpression parameter, ValueProvider<TClass> valueProvider, string? property,
|
||||
ConditionEvaluator conditionEvaluator)
|
||||
{
|
||||
@@ -109,6 +152,66 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
CultureParameter);
|
||||
}
|
||||
|
||||
private static ConditionEvaluator GetPredicate(string? checkString)
|
||||
{
|
||||
if (checkString == null)
|
||||
return DefaultPredicate;
|
||||
|
||||
var match = CheckRegex().Match(checkString);
|
||||
|
||||
var valStr = match.Groups["val"].Value;
|
||||
|
||||
var checkItem = match.Groups["op"].ValueSpan switch
|
||||
{
|
||||
"=" or "" => (v, culture) => VComparedToStr(v, culture, valStr) == 0,
|
||||
"!=" or "!" => (v, culture) => VComparedToStr(v, culture, valStr) != 0,
|
||||
"~" => GetRegExpCheck(valStr),
|
||||
_ => DefaultPredicate,
|
||||
};
|
||||
return (v, culture) => v switch
|
||||
{
|
||||
null => false,
|
||||
IEnumerable<object> e => e.Any(o => checkItem(o, culture)),
|
||||
_ => checkItem(v, culture)
|
||||
};
|
||||
}
|
||||
|
||||
private static int VComparedToStr(object? v, CultureInfo? culture, string valStr)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
return culture.CompareInfo.Compare(v?.ToString()?.Trim(), valStr, CompareOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// build a regular expression check which take the <see cref="CultureInfo"/> into account.
|
||||
/// </summary>
|
||||
/// <param name="valStr"></param>
|
||||
/// <returns>check function to validate an object</returns>
|
||||
private static ConditionEvaluator GetRegExpCheck(string valStr)
|
||||
{
|
||||
return (v, culture) =>
|
||||
{
|
||||
var old = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = culture ?? CultureInfo.CurrentCulture;
|
||||
return Regex.IsMatch(v?.ToString().Trim() ?? "", valStr, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = old;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// without any special check only the existance of the property is checked. Strings need to be non empty.
|
||||
private static readonly ConditionEvaluator DefaultPredicate = (v, _) => v switch
|
||||
{
|
||||
null => false,
|
||||
IEnumerable<object> e => e.Any(),
|
||||
_ => !string.IsNullOrWhiteSpace(v.ToString())
|
||||
};
|
||||
|
||||
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
|
||||
{
|
||||
var match = NameCloseMatcher.Match(templateString);
|
||||
@@ -124,10 +227,24 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData)
|
||||
protected override Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData, OutputType outputType)
|
||||
{
|
||||
var getBool = CreateConditionExpression(matchData.GetValueOrDefault("property")?.Value);
|
||||
var getBool = CreateConditionExpression(
|
||||
matchData.GetValueOrDefault("property")?.Value,
|
||||
Unescape(matchData.GetValueOrDefault("check")));
|
||||
return matchData["not"].Success ? Expression.Not(getBool) : getBool;
|
||||
}
|
||||
|
||||
[GeneratedRegex("""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^\s* # anchor at start of line trimming leading whitespace
|
||||
(?<op> # capture operator in <op> and <numop>
|
||||
~|!=?|=? # - string comparison operators including ~ for regexp. No operator is like =
|
||||
) \s* # ignore space between operator and value
|
||||
(?<val> # capture value in <val>
|
||||
.*? # - string for comparison. May be empty. Non-greedy capture resulting in no whitespace at the end
|
||||
)\s*$ # trimming up to the end
|
||||
""")]
|
||||
private static partial Regex CheckRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ public class NamingTemplate
|
||||
var namingTemplate = new NamingTemplate(tagCollections);
|
||||
try
|
||||
{
|
||||
BinaryNode intermediate = namingTemplate.IntermediateParse(template);
|
||||
Expression evalTree = GetExpressionTree(intermediate);
|
||||
var intermediate = namingTemplate.IntermediateParse(template);
|
||||
var evalTree = GetExpressionTree(intermediate);
|
||||
|
||||
namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile();
|
||||
}
|
||||
@@ -108,11 +108,11 @@ public class NamingTemplate
|
||||
|
||||
BinaryNode topNode = BinaryNode.CreateRoot();
|
||||
BinaryNode? currentNode = topNode;
|
||||
List<char> literalChars = new();
|
||||
List<char> literalChars = [];
|
||||
|
||||
while (templateString.Length > 0)
|
||||
{
|
||||
if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
|
||||
if (StartsWith(templateString, OutputType.String, out var exactPropertyName, out var propertyTag, out var valueExpression))
|
||||
{
|
||||
CheckAndAddLiterals();
|
||||
|
||||
@@ -183,11 +183,12 @@ public class NamingTemplate
|
||||
}
|
||||
}
|
||||
|
||||
private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
|
||||
private bool StartsWith(string template, OutputType outputType, [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))
|
||||
if (pc.StartsWith(template, outputType, out exactName, out propertyTag, out valueExpression))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,51 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returns the value the formatted string. If <c>null</c>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PF.PropertyFormatter<TProperty>? formatter = null)
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PF.PropertyFormatter<TProperty, string>? formatter = null)
|
||||
where TProperty : struct
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a <typeparamref name="TClass"/> property
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returns the value formatted to string. If <c>null</c>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TProperty, string>? formatter = null)
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <typeparam name="TPreFormatted"></typeparam>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="preFormatter">A Func used for first filtering and formatting. The result might be a <see cref="string"/></param>
|
||||
/// <param name="finalizer">This Func assures a string result</param>
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/>
|
||||
public void Add<TProperty, TPreFormatted>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PF.PropertyFormatter<TProperty, TPreFormatted> preFormatter,
|
||||
PF.PropertyFinalizer<TPreFormatted> finalizer)
|
||||
where TProperty : struct
|
||||
=> RegisterWithPreFormatter(templateTag, propertyGetter, preFormatter, finalizer);
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <typeparam name="TPreFormatted"></typeparam>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="preFormatter">A Func used for first filtering and formatting. The result might be a <see cref="string"/></param>
|
||||
/// <param name="finalizer">This Func assures a string result</param>
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/>
|
||||
public void Add<TProperty, TPreFormatted>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TProperty, TPreFormatted> preFormatter,
|
||||
PF.PropertyFinalizer<TPreFormatted> finalizer)
|
||||
=> RegisterWithPreFormatter(templateTag, propertyGetter, preFormatter, finalizer);
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
@@ -56,18 +97,6 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
where TProperty : struct
|
||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||
|
||||
/// <summary>
|
||||
/// Register a <typeparamref name="TClass"/> property
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="templateTag"></param>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returns the value formatted to string. If <c>null</c>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TProperty>? formatter = null)
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
@@ -79,17 +108,33 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||
|
||||
private void RegisterWithFormatter<TProperty, TPropertyValue>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TPropertyValue>? formatter)
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TPropertyValue, string>? formatter)
|
||||
{
|
||||
formatter ??= GetDefaultFormatter<TPropertyValue>();
|
||||
|
||||
if (formatter is null)
|
||||
RegisterWithToString<TProperty, TPropertyValue>(templateTag, propertyGetter, ToStringFunc);
|
||||
else
|
||||
RegisterWithFormatters(templateTag, propertyGetter, formatter, PF.StringFinalizer, PF.ToFinalizer(formatter));
|
||||
}
|
||||
|
||||
private void RegisterWithPreFormatter<TProperty, TPropertyValue, TPreFormatted>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TPropertyValue, TPreFormatted> preFormatter,
|
||||
PF.PropertyFinalizer<TPreFormatted> finalizer)
|
||||
{
|
||||
PF.PropertyFinalizer<TPropertyValue> formatter = PF.ToPropertyFormatter(preFormatter, finalizer);
|
||||
RegisterWithFormatters(templateTag, propertyGetter, preFormatter, finalizer, formatter);
|
||||
}
|
||||
|
||||
private void RegisterWithFormatters<TProperty, TPropertyValue, TPreFormatted>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TPropertyValue, TPreFormatted> preFormatter,
|
||||
PF.PropertyFinalizer<TPreFormatted> finalizer, PF.PropertyFinalizer<TPropertyValue> formatter)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
formatter ??= GetDefaultFormatter<TPropertyValue>();
|
||||
|
||||
AddPropertyTag(formatter is null
|
||||
? new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc)
|
||||
: new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue, TPreFormatted>(templateTag, Options, expr, preFormatter, finalizer, formatter));
|
||||
}
|
||||
|
||||
private void RegisterWithToString<TProperty, TPropertyValue>
|
||||
@@ -99,17 +144,17 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, toString));
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue, string>(templateTag, Options, expr, toString));
|
||||
}
|
||||
|
||||
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
|
||||
|
||||
private PF.PropertyFormatter<T>? GetDefaultFormatter<T>()
|
||||
private PF.PropertyFormatter<T, string>? GetDefaultFormatter<T>()
|
||||
{
|
||||
try
|
||||
{
|
||||
var del = _defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(T)).Value;
|
||||
return del is null ? null : Delegate.CreateDelegate(typeof(PF.PropertyFormatter<T>), del.Target, del.Method) as PF.PropertyFormatter<T>;
|
||||
return del is null ? null : Delegate.CreateDelegate(typeof(PF.PropertyFormatter<T, string>), del.Target, del.Method) as PF.PropertyFormatter<T, string>;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
@@ -119,26 +164,29 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
/// </summary>
|
||||
/// <param name="tagName">Name of the tag value to get</param>
|
||||
/// <param name="object">The property class from which the tag's value is read</param>
|
||||
/// <param name="value"><paramref name="tagName"/>'s string value if it is in this collection, otherwise null</param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="value"><paramref name="tagName"/>'s object value if it is in this collection, otherwise null</param>
|
||||
/// <returns>True if the <paramref name="tagName"/> is in this collection, otherwise false</returns>
|
||||
public bool TryGetValue(string tagName, TClass @object, CultureInfo? culture, [NotNullWhen(true)] out string? value)
|
||||
public bool TryGetObject(string tagName, TClass @object, CultureInfo? culture, out object? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (!StartsWith($"<{tagName}>", out _, out _, out var valueExpression))
|
||||
if (!StartsWith($"<{tagName}>", OutputType.Object, out _, out _, out var valueExpression))
|
||||
return false;
|
||||
|
||||
var func = Expression.Lambda<Func<TClass, CultureInfo?, string>>(valueExpression, Parameter, CultureParameter).Compile();
|
||||
var func = Expression.Lambda<Func<TClass, CultureInfo?, object?>>(valueExpression, Parameter, CultureParameter).Compile();
|
||||
value = func(@object, culture);
|
||||
return true;
|
||||
}
|
||||
|
||||
private class PropertyTag<TPropertyValue> : TagBase
|
||||
private class PropertyTag<TPropertyValue, TPreFormatted> : TagBase
|
||||
{
|
||||
public override Regex NameMatcher { get; }
|
||||
private Func<Expression, string, Expression> CreateToStringExpression { get; }
|
||||
private Func<Expression, string?, Expression> CreateToStringExpression { get; }
|
||||
private Func<Expression, string?, Expression> CreateToObjectExpression { get; }
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter<TPropertyValue> formatter)
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter<TPropertyValue, TPreFormatted> preFormatter,
|
||||
PF.PropertyFinalizer<TPreFormatted> finalizer, PF.PropertyFinalizer<TPropertyValue> formatter)
|
||||
: base(templateTag, propertyGetter)
|
||||
{
|
||||
NameMatcher = new Regex($"""
|
||||
@@ -156,20 +204,45 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
"""
|
||||
, options);
|
||||
|
||||
CreateToObjectExpression = (expVal, format) =>
|
||||
format is null
|
||||
? expVal
|
||||
: Expression.Call(
|
||||
preFormatter.Target is null ? null : Expression.Constant(preFormatter.Target),
|
||||
preFormatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
Expression.Constant(format),
|
||||
CultureParameter);
|
||||
|
||||
CreateToStringExpression = (expVal, format) =>
|
||||
Expression.Call(
|
||||
formatter.Target is null ? null : Expression.Constant(formatter.Target),
|
||||
formatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
Expression.Constant(format),
|
||||
CultureParameter);
|
||||
format is null
|
||||
? Expression.Call(
|
||||
formatter.Target is null ? null : Expression.Constant(formatter.Target),
|
||||
formatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
CultureParameter)
|
||||
: Expression.Call(
|
||||
finalizer.Target is null ? null : Expression.Constant(finalizer.Target),
|
||||
finalizer.Method,
|
||||
Expression.Constant(templateTag),
|
||||
Expression.Call(
|
||||
preFormatter.Target is null ? null : Expression.Constant(preFormatter.Target),
|
||||
preFormatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
Expression.Constant(format),
|
||||
CultureParameter),
|
||||
CultureParameter);
|
||||
}
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString)
|
||||
: base(templateTag, propertyGetter)
|
||||
{
|
||||
NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options);
|
||||
CreateToObjectExpression = (expVal, _) => expVal;
|
||||
|
||||
CreateToStringExpression = (expVal, _) =>
|
||||
Expression.Call(
|
||||
toString.Target is null ? null : Expression.Constant(toString.Target),
|
||||
@@ -177,24 +250,43 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
expVal);
|
||||
}
|
||||
|
||||
protected override Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData)
|
||||
protected override Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData, OutputType outputType)
|
||||
{
|
||||
var formatString = Unescape(matchData.GetValueOrDefault("format")) ?? "";
|
||||
var formatString = Unescape(matchData.GetValueOrDefault("format"));
|
||||
var isReferenceType = !ReturnType.IsValueType;
|
||||
var isNullableValueType = Nullable.GetUnderlyingType(ReturnType) is not null;
|
||||
|
||||
Expression toStringExpression
|
||||
= !ReturnType.IsValueType
|
||||
? Expression.Condition(
|
||||
Expression.Equal(ValueExpression, Expression.Constant(null)),
|
||||
Expression.Constant(""),
|
||||
CreateToStringExpression(ValueExpression, formatString))
|
||||
: Nullable.GetUnderlyingType(ReturnType) is null
|
||||
? CreateToStringExpression(ValueExpression, formatString)
|
||||
: Expression.Condition(
|
||||
Expression.PropertyOrField(ValueExpression, "HasValue"),
|
||||
CreateToStringExpression(Expression.PropertyOrField(ValueExpression, "Value"), formatString),
|
||||
Expression.Constant(""));
|
||||
Expression isNullExpression = isReferenceType
|
||||
? Expression.Equal(ValueExpression, Expression.Constant(null))
|
||||
: isNullableValueType
|
||||
? Expression.Not(Expression.PropertyOrField(ValueExpression, "HasValue"))
|
||||
: Expression.Constant(false);
|
||||
|
||||
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
|
||||
// formatters are defined for non-nullable items <see cref="int"/>, <see cref="DateTime"/> and not for <see cref="int?"/> ...
|
||||
var formattableValueExpression = isNullableValueType
|
||||
? Expression.PropertyOrField(ValueExpression, "Value")
|
||||
: ValueExpression;
|
||||
|
||||
if (outputType == OutputType.String)
|
||||
{
|
||||
Expression toStringExpression =
|
||||
Expression.Condition(
|
||||
isNullExpression,
|
||||
Expression.Constant(null, typeof(string)),
|
||||
CreateToStringExpression(formattableValueExpression, formatString));
|
||||
|
||||
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Expression toObjectExpression =
|
||||
Expression.Condition(
|
||||
isNullExpression,
|
||||
Expression.Constant(null, typeof(object)),
|
||||
Expression.Convert(CreateToObjectExpression(formattableValueExpression, formatString), typeof(object)));
|
||||
|
||||
return Expression.TryCatch(toObjectExpression, Expression.Catch(typeof(Exception), Expression.Constant(null, typeof(object))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
internal enum OutputType
|
||||
{
|
||||
String,
|
||||
Object
|
||||
}
|
||||
|
||||
internal interface IPropertyTag
|
||||
{
|
||||
/// <summary>The tag that will be matched in a tag string</summary>
|
||||
@@ -23,37 +29,33 @@ internal interface IPropertyTag
|
||||
/// Determine if the template string starts with <see cref="TemplateTag"/>, and if it does parse the tag to an <see cref="Expression"/>
|
||||
/// </summary>
|
||||
/// <param name="templateString">Template string</param>
|
||||
/// <param name="outputType">Whether to return a string or object expression</param>
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||
bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
|
||||
bool StartsWith(string templateString, OutputType outputType, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
|
||||
}
|
||||
|
||||
internal abstract class TagBase : IPropertyTag
|
||||
internal abstract class TagBase(ITemplateTag templateTag, Expression propertyExpression) : IPropertyTag
|
||||
{
|
||||
public ITemplateTag TemplateTag { get; }
|
||||
public ITemplateTag TemplateTag { get; } = templateTag;
|
||||
public abstract Regex NameMatcher { get; }
|
||||
public Type ReturnType => ValueExpression.Type;
|
||||
protected Expression ValueExpression { get; }
|
||||
|
||||
protected TagBase(ITemplateTag templateTag, Expression propertyExpression)
|
||||
{
|
||||
TemplateTag = templateTag;
|
||||
ValueExpression = propertyExpression;
|
||||
}
|
||||
protected Expression ValueExpression { get; } = propertyExpression;
|
||||
|
||||
/// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary>
|
||||
/// <param name="exactName">The exact string that was matched to <see cref="ITemplateTag"/></param>
|
||||
/// <param name="matchData">Optional extra data parsed from the tag, such as a format string in the match the square brackets, logical negation, and conditional options</param>
|
||||
protected abstract Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData);
|
||||
/// <param name="outputType">Whether to return a string or object expression</param>
|
||||
protected abstract Expression GetTagExpression(string exactName, Dictionary<string, Group> matchData, OutputType outputType);
|
||||
|
||||
public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
|
||||
public bool StartsWith(string templateString, OutputType outputType, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
|
||||
{
|
||||
var match = NameMatcher.Match(templateString);
|
||||
if (match.Success)
|
||||
{
|
||||
exactName = match.Value;
|
||||
propertyValue = GetTagExpression(exactName, match.Groups.Values.Skip(1).ToDictionary(v => v.Name, v => v));
|
||||
propertyValue = GetTagExpression(exactName, match.Groups.Values.Skip(1).ToDictionary(v => v.Name, v => v), outputType);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,16 +33,17 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
/// and if it does parse the tag to an <see cref="Expression"/>
|
||||
/// </summary>
|
||||
/// <param name="templateString">Template string</param>
|
||||
/// <param name="outputType">Whether to return a string or object expression</param>
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="propertyTag"></param>
|
||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
|
||||
internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag,
|
||||
internal bool StartsWith(string templateString, OutputType outputType, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag,
|
||||
[NotNullWhen(true)] out Expression? propertyValue)
|
||||
{
|
||||
foreach (var p in PropertyTags)
|
||||
{
|
||||
if (p.StartsWith(templateString, out exactName, out propertyValue))
|
||||
if (p.StartsWith(templateString, outputType, out exactName, out propertyValue))
|
||||
{
|
||||
propertyTag = p;
|
||||
return true;
|
||||
|
||||
@@ -31,7 +31,7 @@ public class ContributorDto(string name, string? audibleContributorId) : IFormat
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
return ToString();
|
||||
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements).Trim();
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
|
||||
private static string RemoveSuffix(string namesString)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
@@ -20,36 +21,37 @@ internal partial interface IListFormat<TList> where TList : IListFormat<TList>
|
||||
}
|
||||
}
|
||||
|
||||
static IEnumerable<string> FormattedList<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
|
||||
static IEnumerable<string> FormattedList<T>(string? formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
|
||||
{
|
||||
if (formatString is null) return items.Select(n => n.ToString(null, culture));
|
||||
var format = TList.FormatRegex().Match(formatString).ResolveValue("format");
|
||||
var separator = SeparatorRegex().Match(formatString).ResolveValue("separator");
|
||||
var formattedItems = FilteredList(formatString, items).Select(ItemFormatter);
|
||||
|
||||
// ReSharper disable PossibleMultipleEnumeration
|
||||
return separator is null
|
||||
? formattedItems
|
||||
: formattedItems.Any()
|
||||
? [Join(separator, formattedItems)]
|
||||
: [];
|
||||
// ReSharper restore PossibleMultipleEnumeration
|
||||
if (separator is null) return formattedItems;
|
||||
var joined = Join(separator, formattedItems);
|
||||
return joined is null ? [] : [joined];
|
||||
|
||||
string ItemFormatter(T n) => n.ToString(format, culture);
|
||||
}
|
||||
|
||||
static string Join<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
|
||||
static string? Join(IEnumerable<string>? formattedItems, CultureInfo? culture)
|
||||
{
|
||||
return Join(", ", FormattedList(formatString, items, culture));
|
||||
return formattedItems is null ? null : Join(", ", formattedItems);
|
||||
}
|
||||
|
||||
private static string Join(string separator, IEnumerable<string> strings)
|
||||
private static string? Join(string separator, IEnumerable<string> strings)
|
||||
{
|
||||
return CollapseSpacesRegex().Replace(string.Join(separator, strings), " ");
|
||||
// ReSharper disable PossibleMultipleEnumeration
|
||||
return strings.Any()
|
||||
? CollapseSpacesAndTrimRegex().Replace(string.Join(separator, strings), "")
|
||||
: null;
|
||||
// ReSharper restore PossibleMultipleEnumeration
|
||||
}
|
||||
|
||||
// Collapses runs of 2+ spaces into a single space (does NOT touch tabs/newlines).
|
||||
[GeneratedRegex(@" {2,}")]
|
||||
private static partial Regex CollapseSpacesRegex();
|
||||
// Matches runs of spaces followed by a space as well as runs of spaces at the beginning or the end of a string (does NOT touch tabs/newlines).
|
||||
[GeneratedRegex(@"^ +| +(?=$| )")]
|
||||
private static partial Regex CollapseSpacesAndTrimRegex();
|
||||
|
||||
static abstract Regex FormatRegex();
|
||||
|
||||
|
||||
@@ -9,14 +9,17 @@ namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial class NameListFormat : IListFormat<NameListFormat>
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString, CultureInfo? culture)
|
||||
public static IEnumerable<string> Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string? formatString, CultureInfo? culture)
|
||||
=> names is null
|
||||
? string.Empty
|
||||
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString, ContributorDto.FormatReplacements), culture);
|
||||
? []
|
||||
: IListFormat<NameListFormat>.FormattedList(formatString, Sort(names, formatString, ContributorDto.FormatReplacements), culture);
|
||||
|
||||
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> formatReplacements)
|
||||
public static string? Finalizer(ITemplateTag _, IEnumerable<string>? names, CultureInfo? culture)
|
||||
=> IListFormat<NameListFormat>.Join(names, culture);
|
||||
|
||||
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string? formatString, Dictionary<string, Func<T, object?>> formatReplacements)
|
||||
{
|
||||
var pattern = SortRegex().Match(formatString).ResolveValue("pattern");
|
||||
var pattern = formatString is null ? null : SortRegex().Match(formatString).ResolveValue("pattern");
|
||||
if (pattern is null) return entries;
|
||||
|
||||
IOrderedEnumerable<T>? ordered = null;
|
||||
@@ -42,7 +45,7 @@ internal partial class NameListFormat : IListFormat<NameListFormat>
|
||||
|
||||
private const string Token = @"(?:[TFMLS]|ID)";
|
||||
|
||||
/// <summary> Sort must have at least one of the token labels T, F, M, L, S or ID.Add multiple tokens to sort by multiple fields. Spaces may be used to separate tokens.</summary>
|
||||
/// <summary> Sort must have at least one of the token labels T, F, M, L, S or ID. Use lower case for descending direction and add multiple tokens to sort by multiple fields. Spaces may be used to separate tokens.</summary>
|
||||
[GeneratedRegex($@"[Ss]ort\(\s*(?i:(?<pattern>(?:{Token}\s*?)+))\s*\)")]
|
||||
private static partial Regex SortRegex();
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ public record SeriesDto(string? Name, string? Number, string AudibleSeriesId) :
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
return ToString() ?? string.Empty;
|
||||
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements).Trim();
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,17 @@ namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString, CultureInfo? culture)
|
||||
public static IEnumerable<string> Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string? formatString, CultureInfo? culture)
|
||||
=> series is null
|
||||
? string.Empty
|
||||
: IListFormat<SeriesListFormat>.Join(formatString, Sort(series, formatString, SeriesDto.FormatReplacements), culture);
|
||||
? []
|
||||
: IListFormat<SeriesListFormat>.FormattedList(formatString, Sort(series, formatString, SeriesDto.FormatReplacements), culture);
|
||||
|
||||
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> formatReplacements)
|
||||
public static string? Finalizer(ITemplateTag _, IEnumerable<string>? series, CultureInfo? culture)
|
||||
=> IListFormat<NameListFormat>.Join(series, culture);
|
||||
|
||||
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string? formatString, Dictionary<string, Func<T, object?>> formatReplacements)
|
||||
{
|
||||
var pattern = SortRegex().Match(formatString).ResolveValue("pattern");
|
||||
var pattern = formatString is null ? null : SortRegex().Match(formatString).ResolveValue("pattern");
|
||||
if (pattern is null) return entries;
|
||||
|
||||
IOrderedEnumerable<T>? ordered = null;
|
||||
|
||||
@@ -57,4 +57,5 @@ public sealed class TemplateTags : ITemplateTag
|
||||
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
|
||||
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
|
||||
public static TemplateTags Has { get; } = new TemplateTags("has", "Only include if PROPERTY has a value (i.e. not null or empty)", "<has -><-has>", "<has PROPERTY->...<-has>");
|
||||
public static TemplateTags Is { get; } = new TemplateTags("is", "Only include if PROPERTY has a value satisfying the check (i.e. string comparison)", "<is -><-is>", "<is PROPERTY->...<-is>");
|
||||
}
|
||||
|
||||
@@ -271,11 +271,11 @@ public abstract class Templates
|
||||
{ TemplateTags.TitleShort, lb => GetTitleShort(lb.Title) },
|
||||
{ TemplateTags.AudibleTitle, lb => lb.Title },
|
||||
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
|
||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter },
|
||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter, NameListFormat.Finalizer },
|
||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
|
||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter, NameListFormat.Finalizer },
|
||||
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
|
||||
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter, SeriesListFormat.Finalizer },
|
||||
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.Language, lb => lb.Language },
|
||||
@@ -309,7 +309,7 @@ public abstract class Templates
|
||||
{ TemplateTags.TitleShort, lb => GetTitleShort(lb.Title) },
|
||||
{ TemplateTags.AudibleTitle, lb => lb.Title },
|
||||
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
|
||||
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
|
||||
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter, SeriesListFormat.Finalizer },
|
||||
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, CommonFormatters.FormattableFormatter },
|
||||
},
|
||||
new PropertyTagCollection<MultiConvertFileProperties>(caseSensitive: true, CommonFormatters.StringFormatter, CommonFormatters.IntegerFormatter, CommonFormatters.DateTimeFormatter)
|
||||
@@ -331,6 +331,7 @@ public abstract class Templates
|
||||
|
||||
private static readonly ConditionalTagCollection<CombinedDto> combinedConditionalTags = new()
|
||||
{
|
||||
{ TemplateTags.Is, TryGetValue },
|
||||
{ TemplateTags.Has, TryGetValue, HasValue }
|
||||
};
|
||||
|
||||
@@ -342,11 +343,11 @@ public abstract class Templates
|
||||
private static readonly List<TagCollection> allPropertyTags =
|
||||
chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).ToList();
|
||||
|
||||
private static string? TryGetValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture)
|
||||
private static object? TryGetValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture)
|
||||
{
|
||||
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<LibraryBookDto>>())
|
||||
{
|
||||
if (c.TryGetValue(property, dtos.LibraryBook, culture, out var value))
|
||||
if (c.TryGetObject(property, dtos.LibraryBook, culture, out var value))
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -355,16 +356,22 @@ public abstract class Templates
|
||||
|
||||
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<MultiConvertFileProperties>>())
|
||||
{
|
||||
if (c.TryGetValue(property, dtos.MultiConvert, culture, out var value))
|
||||
if (c.TryGetObject(property, dtos.MultiConvert, culture, out var value))
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool HasValue(string? value, CultureInfo? culture)
|
||||
private static bool HasValue(object? value, CultureInfo? culture)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value);
|
||||
var checkItem = (object o, CultureInfo? _) => !string.IsNullOrWhiteSpace(o.ToString());
|
||||
return value switch
|
||||
{
|
||||
null => false,
|
||||
IEnumerable<object> e => e.Any(o => checkItem(o, culture)),
|
||||
_ => checkItem(value, culture)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -98,16 +98,16 @@ public class GetPortionFilename
|
||||
{ new TemplateTag { TagName = "has3" }, TryGetValue, HasValue }
|
||||
};
|
||||
|
||||
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture)
|
||||
=> props1.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
|
||||
private static object? TryGetValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture)
|
||||
=> props1.TryGetObject(condition, referenceType, culture, out var value) ? value : null;
|
||||
|
||||
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture)
|
||||
=> props2.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
|
||||
private static object? TryGetValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture)
|
||||
=> props2.TryGetObject(condition, referenceType, culture, out var value) ? value : null;
|
||||
|
||||
private static string? TryGetValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture)
|
||||
=> props3.TryGetValue(condition, referenceType, culture, out var value) ? value : null;
|
||||
private static object? TryGetValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture)
|
||||
=> props3.TryGetObject(condition, referenceType, culture, out var value) ? value : null;
|
||||
|
||||
private static bool HasValue(string? value, CultureInfo? culture) => !string.IsNullOrWhiteSpace(value);
|
||||
private static bool HasValue(object? value, CultureInfo? culture) => value is not null && !string.IsNullOrWhiteSpace(value.ToString());
|
||||
|
||||
private readonly PropertyClass1 _propertyClass1 = new()
|
||||
{
|
||||
|
||||
@@ -272,16 +272,15 @@ namespace TemplatesTests
|
||||
[DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 06∕09∕22 00:00.m4b", PlatformID.Unix)]
|
||||
public void DateFormat_illegal(string template, string dirFullPath, string extension, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
if (Environment.OSVersion.Platform != platformId) Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
fileTemplate.HasWarnings.Should().BeFalse();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: CultureInfo.InvariantCulture, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate.HasWarnings.Should().BeFalse();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: CultureInfo.InvariantCulture, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -399,11 +398,29 @@ namespace TemplatesTests
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<has libation version->empty-string<-has>", "")]
|
||||
[DataRow("<is libation version[=foobar]->empty-string<-has>", "")]
|
||||
[DataRow("<is libation version[=]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<is libation version[]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<has file version->null-string<-has>", "")]
|
||||
[DataRow("<has file version[=foobar]->null-string<-has>", "")]
|
||||
[DataRow("<has file version[=]->null-string<-has>", "")]
|
||||
[DataRow("<has file version[]->null-string<-has>", "")]
|
||||
[DataRow("<has year->null-int<-has>", "")]
|
||||
[DataRow("<is year[=]->null-int<-has>", "")]
|
||||
[DataRow("<is year[]->null-int<-has>", "")]
|
||||
[DataRow("<has FAKE->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[=]->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[=foobar]->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[]->unknown-tag<-has>", "")]
|
||||
[DataRow("<has narrator->empty-list<-has>", "")]
|
||||
[DataRow("<has first narrator->no-first<-has>", "")]
|
||||
[DataRow("<is narrator[=foobar]->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[=]->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[~.*]->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[]->empty-list<-has>", "")]
|
||||
[DataRow("<is first narrator->no-first<-has>", "")]
|
||||
[DataRow("<is first narrator[=foobar]->no-first<-has>", "")]
|
||||
[DataRow("<is first narrator[=]->no-first<-has>", "")]
|
||||
[DataRow("<is first narrator[]->no-first<-has>", "")]
|
||||
public void HasValue_on_empty_test(string template, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
@@ -449,6 +466,16 @@ namespace TemplatesTests
|
||||
[DataRow("<has ch title->true<-has>", "true")]
|
||||
[DataRow("<has ch#->true<-has>", "true")]
|
||||
[DataRow("<has ch# 0->true<-has>", "true")]
|
||||
[DataRow("<is title[=A Study in Scarlet: An Audible Original Drama]->true<-has>", "true")]
|
||||
[DataRow("<is title[U][=A STUDY IN SCARLET: AN AUDIBLE ORIGINAL DRAMA]->true<-has>", "true")]
|
||||
[DataRow("<is title[!=foo]->true<-has>", "true")]
|
||||
[DataRow("<is title[~A Study.*]->true<-has>", "true")]
|
||||
[DataRow("<is title[foo]->true<-has>", "")]
|
||||
[DataRow("<is ch count[=2]->true<-has>", "true")]
|
||||
[DataRow("<is author[=Arthur Conan Doyle]->true<-has>", "true")]
|
||||
[DataRow("<is author[format({L})][=Doyle]->true<-has>", "true")]
|
||||
[DataRow("<is author[format({L})separator(:)][=Doyle:Fry]->true<-has>", "true")]
|
||||
[DataRow("<is author[=Sherlock]->true<-has>", "")]
|
||||
public void HasValue_test(string template, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
@@ -529,15 +556,15 @@ namespace TemplatesTests
|
||||
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
|
||||
public void IfSeries_empty(string directory, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series-><-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS {platformId}.");
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series-><-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -545,14 +572,14 @@ namespace TemplatesTests
|
||||
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
|
||||
public void IfSeries_no_series(string directory, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", culture: null, replacements: Replacements)
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -560,15 +587,15 @@ namespace TemplatesTests
|
||||
[DataRow(@"/a/b", @"/a/b/foo-Sherlock Holmes-asin-bar.ext", PlatformID.Unix)]
|
||||
public void IfSeries_with_series(string directory, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -611,7 +638,7 @@ namespace Templates_Other
|
||||
public void Test_trim_to_max_path(string dirFullPath, string template, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
return;
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append('0', 300);
|
||||
@@ -626,7 +653,7 @@ namespace Templates_Other
|
||||
public void Test_windows_relative_path_too_long(string baseDir, string template)
|
||||
{
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
|
||||
return;
|
||||
Assert.Inconclusive($"Skipped because OS is not {PlatformID.Win32NT}.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append('0', 300);
|
||||
@@ -652,8 +679,10 @@ namespace Templates_Other
|
||||
[DataRow(@"/foo/bar/my file.txt", @"/foo/bar/my file - 002 - title.txt", PlatformID.Unix)]
|
||||
public void equiv_GetMultipartFileName(string inStr, string outStr, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
NEW_GetMultipartFileName_FileNamingTemplate(inStr, 2, 100, "title").Should().Be(outStr);
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
NEW_GetMultipartFileName_FileNamingTemplate(inStr, 2, 100, "title").Should().Be(outStr);
|
||||
}
|
||||
|
||||
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
@@ -682,18 +711,18 @@ namespace Templates_Other
|
||||
[DataRow(@"/foo/<title>.txt", @"/foo/s\l∕a\s∕h\e∕s.txt", PlatformID.Unix)]
|
||||
public void remove_slashes(string inStr, string outStr, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
var lbDto = GetLibraryBook();
|
||||
lbDto.TitleWithSubtitle = @"s\l/a\s/h\e/s";
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
var directory = Path.GetDirectoryName(inStr)!;
|
||||
var fileName = Path.GetFileName(inStr);
|
||||
var lbDto = GetLibraryBook();
|
||||
lbDto.TitleWithSubtitle = @"s\l/a\s/h\e/s";
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(fileName, out var fileNamingTemplate).Should().BeTrue();
|
||||
var directory = Path.GetDirectoryName(inStr)!;
|
||||
var fileName = Path.GetFileName(inStr);
|
||||
|
||||
fileNamingTemplate.GetFilename(lbDto, directory, "txt", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(outStr);
|
||||
}
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(fileName, out var fileNamingTemplate).Should().BeTrue();
|
||||
|
||||
fileNamingTemplate.GetFilename(lbDto, directory, "txt", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(outStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -728,13 +757,13 @@ namespace Templates_Folder_Tests
|
||||
[DataRow([@"C:\", new[] { PlatformID.Win32NT }, Templates.ErrorFullPathIsInvalid])]
|
||||
public void Tests(string? template, PlatformID[] platformIds, params string[] expected)
|
||||
{
|
||||
if (platformIds.Contains(Environment.OSVersion.Platform))
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate);
|
||||
var result = folderTemplate.Errors;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
if (!platformIds.Contains(Environment.OSVersion.Platform))
|
||||
Assert.Inconclusive($"Skipped because OS is not one of {platformIds}.");
|
||||
|
||||
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate);
|
||||
var result = folderTemplate.Errors;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,11 +790,11 @@ namespace Templates_Folder_Tests
|
||||
[DataRow(@"<id>\<title>", true, new[] { PlatformID.Win32NT, PlatformID.Unix })]
|
||||
public void Tests(string template, bool expected, PlatformID[] platformIds)
|
||||
{
|
||||
if (platformIds.Contains(Environment.OSVersion.Platform))
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate).Should().BeTrue();
|
||||
folderTemplate.IsValid.Should().Be(expected);
|
||||
}
|
||||
if (!platformIds.Contains(Environment.OSVersion.Platform))
|
||||
Assert.Inconclusive($"Skipped because OS is not one of {platformIds}.");
|
||||
|
||||
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate).Should().BeTrue();
|
||||
folderTemplate.IsValid.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,13 +904,13 @@ namespace Templates_File_Tests
|
||||
|
||||
private void Tests(string? template, PlatformID platformId, params string[] expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate);
|
||||
var result = fileTemplate.Errors;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate);
|
||||
var result = fileTemplate.Errors;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,21 +983,18 @@ namespace Templates_ChapterFile_Tests
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"no tags", null, NamingTemplate.WarningNoTags, Templates.WarningNoChapterNumberTag)]
|
||||
[DataRow(@"<id>\foo\bar", true, Templates.WarningNoChapterNumberTag)]
|
||||
[DataRow(@"<id>/foo/bar", false, Templates.WarningNoChapterNumberTag)]
|
||||
[DataRow(@"<id>\foo\bar", PlatformID.Win32NT, Templates.WarningNoChapterNumberTag)]
|
||||
[DataRow(@"<id>/foo/bar", PlatformID.Unix, Templates.WarningNoChapterNumberTag)]
|
||||
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", null, NamingTemplate.WarningNoTags, Templates.WarningNoChapterNumberTag)]
|
||||
public void Tests(string? template, bool? windows, params string[] expected)
|
||||
public void Tests(string? template, PlatformID? platformId, params string[] expected)
|
||||
{
|
||||
if (windows is null
|
||||
|| (windows is true && Environment.OSVersion.Platform is PlatformID.Win32NT)
|
||||
|| (windows is false && Environment.OSVersion.Platform is PlatformID.Unix))
|
||||
{
|
||||
if (platformId is not null && Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate);
|
||||
var result = chapterFileTemplate.Warnings;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate);
|
||||
var result = chapterFileTemplate.Warnings;
|
||||
result.Should().HaveCount(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,14 +1064,14 @@ namespace Templates_ChapterFile_Tests
|
||||
[DataRow("<ch#>", @"/foo/", "txt", 6, 10, "chap", @"/foo/6.txt", PlatformID.Unix)]
|
||||
public void Tests(string template, string dir, string ext, int pos, int total, string chapter, string expected, PlatformID platformId)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterTemplate).Should().BeTrue();
|
||||
chapterTemplate
|
||||
.GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, culture: null, replacements: Default)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
if (Environment.OSVersion.Platform != platformId)
|
||||
Assert.Inconclusive($"Skipped because OS is not {platformId}.");
|
||||
|
||||
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterTemplate).Should().BeTrue();
|
||||
chapterTemplate
|
||||
.GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, culture: null, replacements: Default)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +56,16 @@ To change how these properties are displayed, [read about custom formatters](#ta
|
||||
|
||||
Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) will only appear in the name if the condition evaluates to true.
|
||||
|
||||
| Tag | Description | Type |
|
||||
| -------------------------------------------------- | ----------------------------------------------------------------- | ----------- |
|
||||
| \<if series-\>...\<-if series\> | Only include if part of a book series or podcast | Conditional |
|
||||
| \<if podcast-\>...\<-if podcast\> | Only include if part of a podcast | Conditional |
|
||||
| \<if bookseries-\>...\<-if bookseries\> | Only include if part of a book series | Conditional |
|
||||
| \<if podcastparent-\>...\<-if podcastparent\>**†** | Only include if item is a podcast series parent | Conditional |
|
||||
| \<has PROPERTY-\>...\<-has\> | Only include if the PROPERTY has a value (i.e. not null or empty) | Conditional |
|
||||
| Tag | Description | Type |
|
||||
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ----------- |
|
||||
| \<if series-\>...\<-if series\> | Only include if part of a book series or podcast | Conditional |
|
||||
| \<if podcast-\>...\<-if podcast\> | Only include if part of a podcast | Conditional |
|
||||
| \<if bookseries-\>...\<-if bookseries\> | Only include if part of a book series | Conditional |
|
||||
| \<if podcastparent-\>...\<-if podcastparent\>**†** | Only include if item is a podcast series parent | Conditional |
|
||||
| \<has PROPERTY-\>...\<-has\> | Only include if the PROPERTY has a value (i.e. not null or empty) | Conditional |
|
||||
| \<is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if the PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<is PROPERTY[FORMAT][[CHECK](#checks)]-\>...\<-is\> | Only include if the formatted PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<is PROPERTY[...separator(...)...][[CHECK](#checks)]-\>...\<-is\> | Only include if the joined form of all formatted values of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
@@ -70,13 +73,14 @@ For example, `<if podcast-><series><-if podcast>` will evaluate to the podcast's
|
||||
|
||||
You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a `!` symbol before the opening tag name.
|
||||
|
||||
| Inverted Tag | Description | Type |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------- | ----------- |
|
||||
| \<!if series-\>...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional |
|
||||
| \<!if podcast-\>...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional |
|
||||
| \<!if bookseries-\>...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional |
|
||||
| \<!if podcastparent-\>...\<-if podcastparent\>**†** | Only include if item is _not_ a podcast series parent | Conditional |
|
||||
| \<!has PROPERTY-\>...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional |
|
||||
| Inverted Tag | Description | Type |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| \<!if series-\>...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional |
|
||||
| \<!if podcast-\>...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional |
|
||||
| \<!if bookseries-\>...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional |
|
||||
| \<!if podcastparent-\>...\<-if podcastparent\>**†** | Only include if item is _not_ a podcast series parent | Conditional |
|
||||
| \<!has PROPERTY-\>...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional |
|
||||
| \<!is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if neither the whole PROPERTY nor the values of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
@@ -173,3 +177,19 @@ You can use custom formatters to construct customized DateTime string. For more
|
||||
|MM|2-digit month|\<file date[MM]\>|02|
|
||||
|dd|2-digit day of the month|\<file date[yyyy-MM-dd]\>|2023-02-14|
|
||||
|HH<br>mm|The hour, using a 24-hour clock from 00 to 23<br>The minute, from 00 through 59.|\<file date[HH:mm]\>|14:45|
|
||||
|
||||
### Checks
|
||||
|
||||
| Check-Pattern | Description | Example |
|
||||
| --------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| =STRING **†** | Matches if one item is equal to STRING (case ignored) | <has tag[=Tag1]-> |
|
||||
| !=STRING **†** | Matches if one item is not equal to STRING (case ignored) | <has first author[!=Arthur]-> |
|
||||
| ~STRING **†** | Matches if one items is matched by the regular expression STRING (case ignored) | <has title[~(\[XYZ\]).*\\1]-> |
|
||||
|
||||
**†** STRING maybe escaped with a backslash. So even square brackets could be used. If a single backslash should be part of the string, it must be doubled.
|
||||
|
||||
#### More complex examples
|
||||
|
||||
This example will truncate the title to 4 characters and check its (trimmed) value to be "the" in any case:
|
||||
|
||||
`<has title[4][=the]>`
|
||||
|
||||
Reference in New Issue
Block a user