mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
As stated in the "Naming Templates" documentation, support custom formatters on numbers.
Only trim spaces on tags where they are disruptive.
This commit is contained in:
@@ -72,7 +72,7 @@ public static class UtilityExtensions
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
LengthInMinutes = libraryBook.Book.LengthInMinutes,
|
||||
Language = libraryBook.Book.Language,
|
||||
Language = libraryBook.Book.Language?.Trim(),
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||
|
||||
@@ -34,7 +34,7 @@ public static partial class CommonFormatters
|
||||
private static string _StringFormatter(string? value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
if (string.IsNullOrEmpty(formatString)) return value;
|
||||
if (string.IsNullOrWhiteSpace(formatString)) return value;
|
||||
|
||||
var match = StringFormatRegex().Match(formatString);
|
||||
if (!match.Success) return value;
|
||||
@@ -60,7 +60,7 @@ public static partial class CommonFormatters
|
||||
|
||||
public static string TemplateStringFormatter<T>(T toFormat, string? templateString, IFormatProvider? provider, Dictionary<string, Func<T, object?>> replacements)
|
||||
{
|
||||
if (string.IsNullOrEmpty(templateString)) return "";
|
||||
if (string.IsNullOrWhiteSpace(templateString)) return "";
|
||||
|
||||
// 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;
|
||||
@@ -89,7 +89,7 @@ public static partial class CommonFormatters
|
||||
// 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.
|
||||
// TemplateStringFormatter will use a dictionary to lookup the tagname and the corresponding value getter.
|
||||
[GeneratedRegex(@"\{(?<tag>[[A-Z]+|#)(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex("""\{(?<tag>[A-Z]+|#)(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex TagFormatRegex();
|
||||
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string? formatString, CultureInfo? culture)
|
||||
@@ -113,7 +113,7 @@ public static partial class CommonFormatters
|
||||
public static string MinutesFormatter(ITemplateTag templateTag, int value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
if (string.IsNullOrWhiteSpace(formatString))
|
||||
return value.ToString(culture);
|
||||
|
||||
var timeSpan = TimeSpan.FromMinutes(value);
|
||||
@@ -148,23 +148,23 @@ public static partial class CommonFormatters
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.InvariantCulture;
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
if (string.IsNullOrWhiteSpace(formatString))
|
||||
formatString = DefaultDateFormat;
|
||||
return value.ToString(formatString, culture);
|
||||
}
|
||||
|
||||
public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
return StringFormatter(templateTag, language?.Trim(), "3u", culture);
|
||||
return StringFormatter(templateTag, language, "3u", culture);
|
||||
}
|
||||
|
||||
// Regex to find patterns like {D:3}, {h:4}, {m}
|
||||
[GeneratedRegex(@"\{D(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex("""\{D(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesD();
|
||||
|
||||
[GeneratedRegex(@"\{H(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex("""\{H(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesH();
|
||||
|
||||
[GeneratedRegex(@"\{M(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex("""\{M(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesM();
|
||||
}
|
||||
@@ -114,11 +114,10 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
(?:\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
|
||||
(?:\s*\[\s* # optional check details enclosed in '[' and ']'. Check shall start with an operator. 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
|
||||
|[^\\\]])* ) # - match any character except '\' and ']'. Check may end in whitespace!
|
||||
\])? # - 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
|
||||
@@ -250,7 +249,7 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
// - Lithuanian locale: 'i' after 'ž' has an accent that affects sorting/matching.
|
||||
//
|
||||
// For naming templates, culture-invariant is the safer default.
|
||||
return regex.IsMatch(v.ToString()?.Trim() ?? "");
|
||||
return regex.IsMatch(v.ToString() ?? "");
|
||||
}
|
||||
catch (RegexMatchTimeoutException ex)
|
||||
{
|
||||
@@ -307,13 +306,12 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
|
||||
[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>(?<num_op> # capture operator in <op> and <num_op> with every char escapable
|
||||
^(?<op>(?<num_op> # anchor at start of linecapture operator in <op> and <num_op> with every char escapable
|
||||
\\?\#(?:\\?!)?\\?= # - numerical operators: #= #!=
|
||||
| \\?\#\\?[<>](?:\\?=)? # - numerical operators: #>= #<= #> #<
|
||||
| \\?[<>](?:\\?=)? # - numerical operators: >= <= > <
|
||||
) | \\?~|\\?!(?:\\?=)?|(?:\\?=)? # - string comparison operators including ~ for regexp, = and !=. No operator is like =
|
||||
) \s* # ignore space between operator and value
|
||||
) \s*? # ignore space between operator and value
|
||||
(?<val>(?(num_op) # capture value in <val>
|
||||
(?:\\?\d)+ # - numerical operators have to be followed by a number
|
||||
| (?:\\.|[^\\])* ) # - string for comparison. May be empty. Capturing also all whitespace up to the end as this must have been escaped.
|
||||
|
||||
@@ -189,17 +189,17 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
: base(templateTag, propertyGetter)
|
||||
{
|
||||
NameMatcher = new Regex($"""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^< # tags start with a '<'
|
||||
{TagNameForRegex()} # next the tagname needs to be matched with space being made optional. Also escape all '#'
|
||||
(?:\s* # optional whitespace
|
||||
\[\s* # optional format details enclosed in '[' and ']'. Format shall be trimmed. So match whitespace first
|
||||
(?<format> # - capture inner part as <format>
|
||||
(?:\\. # - '\' 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 '>'
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^< # tags start with a '<'
|
||||
{TagNameForRegex()} # next the tagname needs to be matched with space being made optional. Also escape all '#'
|
||||
(?:\s* # optional whitespace
|
||||
\[ (?<format> # optional format details enclosed in '[' and ']'. Capture inner part as <format>.
|
||||
(?:\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
|'(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
|"(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
|[^\\\]])* ) # - match any character except '\' and ']'. Format may end in whitespace!
|
||||
\] # - closing the format part
|
||||
)?\s*> # Tags end with '>'
|
||||
"""
|
||||
, options);
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ namespace TemplatesTests
|
||||
[DataRow("<bitrate[2]>Kbps <titleshort[u]>", "128Kbps A STUDY IN SCARLET")]
|
||||
[DataRow("<bitrate[3]>Kbps <titleshort[t]>", "128Kbps A Study In Scarlet")]
|
||||
[DataRow("<bitrate[4]>Kbps <titleshort[l]>", "0128Kbps a study in scarlet")]
|
||||
[DataRow(@"<bitrate[00'['0\\#0']']>Kbps <titleshort[T]>", "01[2#8]Kbps A Study In Scarlet")]
|
||||
[DataRow("<codec[7t]> <samplerate[6]>Hz", "Aac[Lc] 044100Hz")]
|
||||
[DataRow("<codec[3T]> <titleshort[ 5 U ]>", "AAC A STU")]
|
||||
[DataRow("<bitrate [ 4 ] >Kbps <samplerate [ 6 ] >Hz", "0128Kbps 044100Hz")]
|
||||
@@ -208,6 +209,7 @@ namespace TemplatesTests
|
||||
[DataRow("<minutes[{d}-{m}]>", 2000, "1-560")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 2880, "2-0")]
|
||||
[DataRow("<minutes[{d:2}-{m:2}]>", 1500, "01-60")]
|
||||
[DataRow(@"<minutes[{d:0}-{m:000'{'00\}}]>", 2000, "1-005{60}")]
|
||||
public void MinutesFormat(string template, int minutes, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
@@ -557,14 +559,14 @@ namespace TemplatesTests
|
||||
[DataRow("<is tag[=Tag1]->true<-has>", "true")]
|
||||
[DataRow("<is tag[separator(:)slice(-2..)][=Tag2:Tag3]->true<-has>", "true")]
|
||||
[DataRow("<is audible subtitle[3][=an]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][=an ]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][=an ]->true<-has>", "true")]
|
||||
[DataRow(@"<is audible subtitle[3][=an\ ]->true<-has>", "true")]
|
||||
[DataRow("<is audible subtitle[3][= an]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][= an ]->false<-has>", "")]
|
||||
[DataRow(@"<is audible subtitle[3][= an\ ]->true<-has>", "true")]
|
||||
[DataRow(@"<is audible subtitle[3][= an\ ]->false<-has>", "")]
|
||||
[DataRow(@"<is audible subtitle[3][=\ an\ ]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][ =an]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][ =an ]->false<-has>", "")]
|
||||
[DataRow("<is audible subtitle[3][ =an ]->true<-has>", "true")]
|
||||
[DataRow(@"<is audible subtitle[3][ =an\ ]->true<-has>", "true")]
|
||||
public void HasValue_test(string template, string expected)
|
||||
{
|
||||
@@ -598,7 +600,7 @@ namespace TemplatesTests
|
||||
[DataRow("<first series>", "Series A")]
|
||||
[DataRow("<first series[]>", "Series A")]
|
||||
[DataRow("<first series[{N}, {#}, {ID}]>", "Series A, 1, B1")]
|
||||
[DataRow("<first series[{N}, {#:00.0}]>", "Series A, 01.0")]
|
||||
[DataRow("<first series[{N}, {#:0'{}'0.0}]>", "Series A, 0{}1.0")]
|
||||
public void SeriesFormat_formatters(string template, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
|
||||
Reference in New Issue
Block a user