Regular Expressions with capture names on coments

This commit is contained in:
Jo-Be-Co
2026-03-13 20:02:06 +01:00
parent 508ea1032d
commit db3a810f47
6 changed files with 76 additions and 26 deletions

View File

@@ -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<TClass>(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($"^<(?<not>!)?{tagNameRe}->", options | RegexOptions.Compiled);
NameCloseMatcher = new Regex($"^<-{tagNameRe}>", options | RegexOptions.Compiled);
CreateConditionExpression = _ => conditionExpression;
}
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, Conditional<TClass> conditional)
: base(templateTag, Expression.Constant(false))
{
NameMatcher = new Regex(@$"^<(!)?{templateTag.TagName}(?:\s+?(.*?)\s*?)?->", options);
// <property> 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 #
^<(?<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)
)? # 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

View File

@@ -141,7 +141,19 @@ public class PropertyTagCollection<TClass> : TagCollection
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter<TPropertyValue> 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 ']'.
(?<format> # - capture inner part as <format>
[^\]]*? # - 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<TClass> : TagCollection
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> 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),

View File

@@ -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}]";

View File

@@ -14,8 +14,7 @@ internal partial interface IListFormat<TList> where TList : IListFormat<TList>
static IEnumerable<T> Max(string formatString, IEnumerable<T> 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<TList> where TList : IListFormat<TList>
static IEnumerable<string> FormattedList<T>(string formatString, IEnumerable<T> 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<TList> where TList : IListFormat<TList>
// ReSharper restore PossibleMultipleEnumeration
string ItemFormatter(T n) => n.ToString(format, culture);
static string? FormatElement(string formatString, Func<Regex> regex)
{
var match = regex().Match(formatString);
return match.Success ? match.Groups[1].Value : null;
}
}
static string Join<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
@@ -61,10 +54,38 @@ internal partial interface IListFormat<TList> where TList : IListFormat<TList>
static abstract Regex FormatRegex();
/// <summary> Max must have a 1 or 2-digit number </summary>
[GeneratedRegex(@"[Mm]ax\(\s*([1-9]\d?)\s*\)")]
[GeneratedRegex(@"[Mm]ax\(\s*(?<max>[1-9]\d?)\s*\)")]
private static partial Regex MaxRegex();
/// <summary> Separator can be anything </summary>
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
[GeneratedRegex(@"[Ss]eparator\((?<separator>.*?)\)")]
private static partial Regex SeparatorRegex();
}
static class RegExpExtensions
{
extension(Group group)
{
public string? ValueOrNull() => group.Success ? group.Value : null;
public ReadOnlySpan<char> 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);
}
}
}

View File

@@ -16,11 +16,11 @@ internal partial class NameListFormat : IListFormat<NameListFormat>
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> 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<T>? 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<NameListFormat>
private static partial Regex SortTokenizer();
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} </summary>
[GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
[GeneratedRegex($@"[Ff]ormat\((?<format>.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@@ -16,11 +16,11 @@ internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> 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<T>? 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<SeriesListFormat>
private static partial Regex SortTokenizer();
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
[GeneratedRegex($@"[Ff]ormat\((?<format>.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
public static partial Regex FormatRegex();
}