diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 83f1aa27..a515bdb8 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -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, diff --git a/Source/FileManager/NamingTemplate/CommonFormatters.cs b/Source/FileManager/NamingTemplate/CommonFormatters.cs index 40bea574..cf15dbdb 100644 --- a/Source/FileManager/NamingTemplate/CommonFormatters.cs +++ b/Source/FileManager/NamingTemplate/CommonFormatters.cs @@ -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 toFormat, string? templateString, IFormatProvider? provider, Dictionary> 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(@"\{(?[[A-Z]+|#)(?::(?.*?))?\}", RegexOptions.IgnoreCase)] + [GeneratedRegex("""\{(?[A-Z]+|#)(?::(?(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", 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(?::(?.*?))?\}", RegexOptions.IgnoreCase)] + [GeneratedRegex("""\{D(?::(?(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)] private static partial Regex RegexMinutesD(); - [GeneratedRegex(@"\{H(?::(?.*?))?\}", RegexOptions.IgnoreCase)] + [GeneratedRegex("""\{H(?::(?(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)] private static partial Regex RegexMinutesH(); - [GeneratedRegex(@"\{M(?::(?.*?))?\}", RegexOptions.IgnoreCase)] + [GeneratedRegex("""\{M(?::(?(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)] private static partial Regex RegexMinutesM(); } \ No newline at end of file diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index 56884c35..98570b0e 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -114,11 +114,10 @@ public partial class ConditionalTagCollection(bool caseSensitive = true) (?:\s+ # the following part is optional. If present it starts with some whitespace (?.+? # - capture the non greedy so it won't end on whitespace, '[' or '-' (if match is possible) (? end with a whitepace. Otherwise "" 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 (? # - capture inner part as (?:\\. # - '\' 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(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(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 - (?(? # capture operator in and with every char escapable + ^(?(? # anchor at start of linecapture operator in and 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 (?(?(num_op) # capture value in (?:\\?\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. diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 6b262d61..9d21cb59 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -189,17 +189,17 @@ public class PropertyTagCollection : 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 - (? # - capture inner part as - (?:\\. # - '\' 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 + \[ (? # optional format details enclosed in '[' and ']'. Capture inner part as . + (?:\\. # - '\' 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); diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 1fbb81ac..112fd0e8 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -167,6 +167,7 @@ namespace TemplatesTests [DataRow("Kbps ", "128Kbps A STUDY IN SCARLET")] [DataRow("Kbps ", "128Kbps A Study In Scarlet")] [DataRow("Kbps ", "0128Kbps a study in scarlet")] + [DataRow(@"Kbps ", "01[2#8]Kbps A Study In Scarlet")] [DataRow(" Hz", "Aac[Lc] 044100Hz")] [DataRow(" ", "AAC A STU")] [DataRow("Kbps Hz", "0128Kbps 044100Hz")] @@ -208,6 +209,7 @@ namespace TemplatesTests [DataRow("", 2000, "1-560")] [DataRow("", 2880, "2-0")] [DataRow("", 1500, "01-60")] + [DataRow(@"", 2000, "1-005{60}")] public void MinutesFormat(string template, int minutes, string expected) { var bookDto = GetLibraryBook(); @@ -557,14 +559,14 @@ namespace TemplatesTests [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("false<-has>", "")] - [DataRow("false<-has>", "")] + [DataRow("true<-has>", "true")] [DataRow(@"true<-has>", "true")] [DataRow("false<-has>", "")] [DataRow("false<-has>", "")] - [DataRow(@"true<-has>", "true")] + [DataRow(@"false<-has>", "")] [DataRow(@"false<-has>", "")] [DataRow("false<-has>", "")] - [DataRow("false<-has>", "")] + [DataRow("true<-has>", "true")] [DataRow(@"true<-has>", "true")] public void HasValue_test(string template, string expected) { @@ -598,7 +600,7 @@ namespace TemplatesTests [DataRow("", "Series A")] [DataRow("", "Series A")] [DataRow("", "Series A, 1, B1")] - [DataRow("", "Series A, 01.0")] + [DataRow("", "Series A, 0{}1.0")] public void SeriesFormat_formatters(string template, string expected) { var bookDto = GetLibraryBook();