mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-03-24 23:45:30 +00:00
Rework architecture
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal class AggregateMatcher<T> : IMatcher<T>
|
||||
{
|
||||
private readonly IReadOnlyList<IMatcher<T>> _matchers;
|
||||
|
||||
public AggregateMatcher(IReadOnlyList<IMatcher<T>> matchers)
|
||||
{
|
||||
_matchers = matchers;
|
||||
}
|
||||
|
||||
public AggregateMatcher(params IMatcher<T>[] matchers)
|
||||
: this((IReadOnlyList<IMatcher<T>>) matchers)
|
||||
{
|
||||
}
|
||||
|
||||
public ParsedMatch<T>? TryMatch(StringPart stringPart)
|
||||
{
|
||||
ParsedMatch<T>? earliestMatch = null;
|
||||
|
||||
// Try to match the input with each matcher and get the match with the lowest start index
|
||||
foreach (var matcher in _matchers)
|
||||
{
|
||||
// Try to match
|
||||
var match = matcher.TryMatch(stringPart);
|
||||
|
||||
// If there's no match - continue
|
||||
if (match == null)
|
||||
continue;
|
||||
|
||||
// If this match is earlier than previous earliest - replace
|
||||
if (earliestMatch == null || match.StringPart.StartIndex < earliestMatch.StringPart.StartIndex)
|
||||
earliestMatch = match;
|
||||
|
||||
// If the earliest match starts at the very beginning - break,
|
||||
// because it's impossible to find a match earlier than that
|
||||
if (earliestMatch.StringPart.StartIndex == stringPart.StartIndex)
|
||||
break;
|
||||
}
|
||||
|
||||
return earliestMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
DiscordChatExporter.Domain/Markdown/Matching/IMatcher.cs
Normal file
49
DiscordChatExporter.Domain/Markdown/Matching/IMatcher.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal interface IMatcher<T>
|
||||
{
|
||||
ParsedMatch<T>? TryMatch(StringPart stringPart);
|
||||
}
|
||||
|
||||
internal static class MatcherExtensions
|
||||
{
|
||||
public static IEnumerable<ParsedMatch<T>> MatchAll<T>(this IMatcher<T> matcher,
|
||||
StringPart stringPart, Func<StringPart, T> transformFallback)
|
||||
{
|
||||
// Loop through segments divided by individual matches
|
||||
var currentIndex = stringPart.StartIndex;
|
||||
while (currentIndex < stringPart.EndIndex)
|
||||
{
|
||||
// Find a match within this segment
|
||||
var match = matcher.TryMatch(stringPart.Slice(currentIndex, stringPart.EndIndex - currentIndex));
|
||||
|
||||
// If there's no match - break
|
||||
if (match == null)
|
||||
break;
|
||||
|
||||
// If this match doesn't start immediately at current index - transform and yield fallback first
|
||||
if (match.StringPart.StartIndex > currentIndex)
|
||||
{
|
||||
var fallbackPart = stringPart.Slice(currentIndex, match.StringPart.StartIndex - currentIndex);
|
||||
yield return new ParsedMatch<T>(fallbackPart, transformFallback(fallbackPart));
|
||||
}
|
||||
|
||||
// Yield match
|
||||
yield return match;
|
||||
|
||||
// Shift current index to the end of the match
|
||||
currentIndex = match.StringPart.StartIndex + match.StringPart.Length;
|
||||
}
|
||||
|
||||
// If EOL wasn't reached - transform and yield remaining part as fallback
|
||||
if (currentIndex < stringPart.EndIndex)
|
||||
{
|
||||
var fallbackPart = stringPart.Slice(currentIndex);
|
||||
yield return new ParsedMatch<T>(fallbackPart, transformFallback(fallbackPart));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
DiscordChatExporter.Domain/Markdown/Matching/ParsedMatch.cs
Normal file
15
DiscordChatExporter.Domain/Markdown/Matching/ParsedMatch.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal class ParsedMatch<T>
|
||||
{
|
||||
public StringPart StringPart { get; }
|
||||
|
||||
public T Value { get; }
|
||||
|
||||
public ParsedMatch(StringPart stringPart, T value)
|
||||
{
|
||||
StringPart = stringPart;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
DiscordChatExporter.Domain/Markdown/Matching/RegexMatcher.cs
Normal file
40
DiscordChatExporter.Domain/Markdown/Matching/RegexMatcher.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal class RegexMatcher<T> : IMatcher<T>
|
||||
{
|
||||
private readonly Regex _regex;
|
||||
private readonly Func<StringPart, Match, T> _transform;
|
||||
|
||||
public RegexMatcher(Regex regex, Func<StringPart, Match, T> transform)
|
||||
{
|
||||
_regex = regex;
|
||||
_transform = transform;
|
||||
}
|
||||
|
||||
public RegexMatcher(Regex regex, Func<Match, T> transform)
|
||||
: this(regex, (p, m) => transform(m))
|
||||
{
|
||||
}
|
||||
|
||||
public ParsedMatch<T>? TryMatch(StringPart stringPart)
|
||||
{
|
||||
var match = _regex.Match(stringPart.Target, stringPart.StartIndex, stringPart.Length);
|
||||
if (!match.Success)
|
||||
return null;
|
||||
|
||||
// Overload regex.Match(string, int, int) doesn't take the whole string into account,
|
||||
// it effectively functions as a match check on a substring.
|
||||
// Which is super weird because regex.Match(string, int) takes the whole input in context.
|
||||
// So in order to properly account for ^/$ regex tokens, we need to make sure that
|
||||
// the expression also matches on the bigger part of the input.
|
||||
if (!_regex.IsMatch(stringPart.Target.Substring(0, stringPart.EndIndex), stringPart.StartIndex))
|
||||
return null;
|
||||
|
||||
var stringPartMatch = stringPart.Slice(match.Index, match.Length);
|
||||
return new ParsedMatch<T>(stringPartMatch, _transform(stringPartMatch, match));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal class StringMatcher<T> : IMatcher<T>
|
||||
{
|
||||
private readonly string _needle;
|
||||
private readonly StringComparison _comparison;
|
||||
private readonly Func<StringPart, T> _transform;
|
||||
|
||||
public StringMatcher(string needle, StringComparison comparison, Func<StringPart, T> transform)
|
||||
{
|
||||
_needle = needle;
|
||||
_comparison = comparison;
|
||||
_transform = transform;
|
||||
}
|
||||
|
||||
public StringMatcher(string needle, Func<StringPart, T> transform)
|
||||
: this(needle, StringComparison.Ordinal, transform)
|
||||
{
|
||||
}
|
||||
|
||||
public ParsedMatch<T>? TryMatch(StringPart stringPart)
|
||||
{
|
||||
var index = stringPart.Target.IndexOf(_needle, stringPart.StartIndex, stringPart.Length, _comparison);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
var stringPartMatch = stringPart.Slice(index, _needle.Length);
|
||||
return new ParsedMatch<T>(stringPartMatch, _transform(stringPartMatch));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
DiscordChatExporter.Domain/Markdown/Matching/StringPart.cs
Normal file
36
DiscordChatExporter.Domain/Markdown/Matching/StringPart.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
||||
{
|
||||
internal readonly struct StringPart
|
||||
{
|
||||
public string Target { get; }
|
||||
|
||||
public int StartIndex { get; }
|
||||
|
||||
public int Length { get; }
|
||||
|
||||
public int EndIndex { get; }
|
||||
|
||||
public StringPart(string target, int startIndex, int length)
|
||||
{
|
||||
Target = target;
|
||||
StartIndex = startIndex;
|
||||
Length = length;
|
||||
EndIndex = startIndex + length;
|
||||
}
|
||||
|
||||
public StringPart(string target)
|
||||
: this(target, 0, target.Length)
|
||||
{
|
||||
}
|
||||
|
||||
public StringPart Slice(int newStartIndex, int newLength) => new StringPart(Target, newStartIndex, newLength);
|
||||
|
||||
public StringPart Slice(int newStartIndex) => Slice(newStartIndex, EndIndex - newStartIndex);
|
||||
|
||||
public StringPart Slice(Capture capture) => Slice(capture.Index, capture.Length);
|
||||
|
||||
public override string ToString() => Target.Substring(StartIndex, Length);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user