From db3a810f47be0f5f64d24b4e8c1c705b56f7f470 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Fri, 13 Mar 2026 20:02:06 +0100 Subject: [PATCH] Regular Expressions with capture names on coments --- .../ConditionalTagCollection[TClass].cs | 20 +++++++-- .../PropertyTagCollection[TClass].cs | 16 ++++++- Source/FileManager/NamingTemplate/TagBase.cs | 5 +++ .../Templates/IListFormat[TList].cs | 45 ++++++++++++++----- .../Templates/NameListFormat.cs | 8 ++-- .../Templates/SeriesListFormat.cs | 8 ++-- 6 files changed, 76 insertions(+), 26 deletions(-) diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index ce98ce10..2fa20906 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -1,4 +1,5 @@ -using System; + +using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; @@ -58,15 +59,26 @@ public class ConditionalTagCollection(bool caseSensitive = true) : TagCo public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression) : base(templateTag, conditionExpression) { - NameMatcher = new Regex(@$"^<(!)?{templateTag.TagName}->", options | RegexOptions.Compiled); - NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options | RegexOptions.Compiled); + var tagNameRe = TagNameForRegex(); + NameMatcher = new Regex($"^<(?!)?{tagNameRe}->", options | RegexOptions.Compiled); + NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options | RegexOptions.Compiled); CreateConditionExpression = _ => conditionExpression; } public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, Conditional conditional) : base(templateTag, Expression.Constant(false)) { - NameMatcher = new Regex(@$"^<(!)?{templateTag.TagName}(?:\s+?(.*?)\s*?)?->", options); + // needs to match on at least one character which is not a space + NameMatcher = new Regex($""" + (?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with # + ^<(?!)? # tags start with a '<'. Condtionals allow an optional ! captured in 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 + (?.+?) # - capture the non greedy so it won't end on whitespace, '[' or '-' (if match is possible) + )? # end of optional property and check part + \s*-> # Opening tags end with '->' and closing tags begin with '<-', so both sides visually point toward each other + """ + , options | RegexOptions.Compiled); NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options | RegexOptions.Compiled); CreateConditionExpression = condition diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index ced3bb81..f2a64e6b 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -141,7 +141,19 @@ public class PropertyTagCollection : TagCollection public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter formatter) : base(templateTag, propertyGetter) { - NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options | RegexOptions.Compiled); + 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 + \[ # optional format details enclosed in '[' and ']'. + (? # - capture inner part as + [^\]]*? # - match any character except ']' + ) # + \] # - closing the format part + )?\s*> # Tags end with '>' + """ + , options | RegexOptions.Compiled); CreateToStringExpression = (expVal, format) => Expression.Call( formatter.Target is null ? null : Expression.Constant(formatter.Target), @@ -155,7 +167,7 @@ public class PropertyTagCollection : TagCollection public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func toString) : base(templateTag, propertyGetter) { - NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}>", options | RegexOptions.Compiled); + NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options | RegexOptions.Compiled); CreateToStringExpression = (expVal, _) => Expression.Call( toString.Target is null ? null : Expression.Constant(toString.Target), diff --git a/Source/FileManager/NamingTemplate/TagBase.cs b/Source/FileManager/NamingTemplate/TagBase.cs index af80d5f4..201acfb1 100644 --- a/Source/FileManager/NamingTemplate/TagBase.cs +++ b/Source/FileManager/NamingTemplate/TagBase.cs @@ -60,6 +60,11 @@ internal abstract class TagBase : IPropertyTag return false; } + protected string TagNameForRegex() + { + return TemplateTag.TagName.Replace(" ", @"\s*").Replace("#", @"\#"); + } + public override string ToString() { return $"[Name = {TemplateTag.TagName}, Type = {ReturnType.Name}]"; diff --git a/Source/LibationFileManager/Templates/IListFormat[TList].cs b/Source/LibationFileManager/Templates/IListFormat[TList].cs index a18f0315..b76929e3 100644 --- a/Source/LibationFileManager/Templates/IListFormat[TList].cs +++ b/Source/LibationFileManager/Templates/IListFormat[TList].cs @@ -14,8 +14,7 @@ internal partial interface IListFormat where TList : IListFormat static IEnumerable Max(string formatString, IEnumerable items) { - var maxMatch = MaxRegex().Match(formatString); - return maxMatch.Success && int.TryParse(maxMatch.Groups[1].ValueSpan, out var max) + return MaxRegex().Match(formatString).TryParseInt("max", out var max) ? items.Take(max) : items; } @@ -23,8 +22,8 @@ internal partial interface IListFormat where TList : IListFormat static IEnumerable FormattedList(string formatString, IEnumerable items, CultureInfo? culture) where T : IFormattable { - var format = FormatElement(formatString, TList.FormatRegex); - var separator = FormatElement(formatString, SeparatorRegex); + 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 @@ -36,12 +35,6 @@ internal partial interface IListFormat where TList : IListFormat // ReSharper restore PossibleMultipleEnumeration string ItemFormatter(T n) => n.ToString(format, culture); - - static string? FormatElement(string formatString, Func regex) - { - var match = regex().Match(formatString); - return match.Success ? match.Groups[1].Value : null; - } } static string Join(string formatString, IEnumerable items, CultureInfo? culture) where T : IFormattable @@ -61,10 +54,38 @@ internal partial interface IListFormat where TList : IListFormat static abstract Regex FormatRegex(); /// Max must have a 1 or 2-digit number - [GeneratedRegex(@"[Mm]ax\(\s*([1-9]\d?)\s*\)")] + [GeneratedRegex(@"[Mm]ax\(\s*(?[1-9]\d?)\s*\)")] private static partial Regex MaxRegex(); /// Separator can be anything - [GeneratedRegex(@"[Ss]eparator\((.*?)\)")] + [GeneratedRegex(@"[Ss]eparator\((?.*?)\)")] private static partial Regex SeparatorRegex(); } + +static class RegExpExtensions +{ + extension(Group group) + { + public string? ValueOrNull() => group.Success ? group.Value : null; + public ReadOnlySpan ValueSpanOrNull() => group.Success ? group.ValueSpan : null; + } + + extension(Match match) + { + public Group Resolve(string? groupName = null) + { + if (groupName is not null && match.Groups.TryGetValue(groupName, out var group)) + return group; + return match.Groups.Count > 1 ? match.Groups[1] : match.Groups[0]; + } + + public string? ResolveValue(string? groupName = null) => match.Resolve(groupName).ValueOrNull(); + + public bool TryParseInt(string? groupName, out int value) + { + var span = match.Resolve(groupName).ValueSpanOrNull(); + + return int.TryParse(span, out value); + } + } +} diff --git a/Source/LibationFileManager/Templates/NameListFormat.cs b/Source/LibationFileManager/Templates/NameListFormat.cs index e50e7a08..823bbe5c 100644 --- a/Source/LibationFileManager/Templates/NameListFormat.cs +++ b/Source/LibationFileManager/Templates/NameListFormat.cs @@ -16,11 +16,11 @@ internal partial class NameListFormat : IListFormat private static IEnumerable Sort(IEnumerable entries, string formatString, Dictionary> formatReplacements) { - var sortMatch = SortRegex().Match(formatString); - if (!sortMatch.Success) return entries; + var pattern = SortRegex().Match(formatString).ResolveValue("pattern"); + if (pattern is null) return entries; IOrderedEnumerable? ordered = null; - foreach (Match m in SortTokenizer().Matches(sortMatch.Groups["pattern"].Value)) + foreach (Match m in SortTokenizer().Matches(pattern!)) { // Dictionary is case-insensitive, no ToUpper needed if (!formatReplacements.TryGetValue(m.Groups["token"].Value, out var selector)) @@ -50,6 +50,6 @@ internal partial class NameListFormat : IListFormat private static partial Regex SortTokenizer(); /// Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} - [GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")] + [GeneratedRegex($@"[Ff]ormat\((?.*?\{{{Token}(?::.*?)?\}}.*?)\)")] public static partial Regex FormatRegex(); } diff --git a/Source/LibationFileManager/Templates/SeriesListFormat.cs b/Source/LibationFileManager/Templates/SeriesListFormat.cs index c7390970..2f7fc384 100644 --- a/Source/LibationFileManager/Templates/SeriesListFormat.cs +++ b/Source/LibationFileManager/Templates/SeriesListFormat.cs @@ -16,11 +16,11 @@ internal partial class SeriesListFormat : IListFormat private static IEnumerable Sort(IEnumerable entries, string formatString, Dictionary> formatReplacements) { - var sortMatch = SortRegex().Match(formatString); - if (!sortMatch.Success) return entries; + var pattern = SortRegex().Match(formatString).ResolveValue("pattern"); + if (pattern is null) return entries; IOrderedEnumerable? ordered = null; - foreach (Match m in SortTokenizer().Matches(sortMatch.Groups["pattern"].Value)) + foreach (Match m in SortTokenizer().Matches(pattern!)) { // Dictionary is case-insensitive, no ToUpper needed if (!formatReplacements.TryGetValue(m.Groups["token"].Value, out var selector)) @@ -50,6 +50,6 @@ internal partial class SeriesListFormat : IListFormat private static partial Regex SortTokenizer(); /// Format must have at least one of the string {N}, {#}, {ID} - [GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")] + [GeneratedRegex($@"[Ff]ormat\((?.*?\{{{Token}(?::.*?)?\}}.*?)\)")] public static partial Regex FormatRegex(); }