Iceshrimp.NET/Iceshrimp.Parsing/SearchQueryParser.cs

103 lines
3.1 KiB
C#

namespace Iceshrimp.Parsing;
public static class SearchQueryParser
{
public static List<ISearchQueryFilter> Parse(ReadOnlySpan<char> input)
{
var results = new List<ISearchQueryFilter>();
input = input.Trim();
if (input.Length == 0) return [];
int pos = 0;
while (pos < input.Length)
{
var oldPos = pos;
var res = ParseToken(input, ref pos);
if (res == null) return results;
if (pos <= oldPos) throw new Exception("Infinite loop detected!");
results.Add(res);
}
return results;
}
private static ISearchQueryFilter? ParseToken(ReadOnlySpan<char> input, ref int pos)
{
while (input[pos] == ' ')
{
pos++;
if (pos >= input.Length) return null;
}
var negated = false;
if (input[pos] == '-' && input.Length > pos + 1)
{
negated = true;
pos++;
}
if (input[pos] == '"' && input.Length > pos + 2)
{
var closingQuote = pos + 1 + input[(pos + 1)..].IndexOf('"');
if (closingQuote != -1)
{
var literalRes = new WordFilter(negated, input[++pos..closingQuote].ToString());
pos = closingQuote + 1;
return literalRes;
}
}
if (input[pos] == '(' && input.Length > pos + 2)
{
var closingParen = pos + 1 + input[(pos + 1)..].IndexOf(')');
if (closingParen != -1)
{
var items = input[++pos..closingParen].ToString().Split(" OR ").Select(p => p.Trim()).ToArray();
var literalRes = new MultiWordFilter(negated, items);
if (items.Length > 0)
{
pos = closingParen + 1;
return literalRes;
}
}
}
var end = input[pos..].IndexOf(' ');
if (end == -1)
end = input.Length;
else
end += pos;
var splitIdx = input[pos..end].IndexOf(':');
var keyRange = splitIdx < 1 ? ..0 : pos..(pos + splitIdx);
var key = splitIdx < 1 ? ReadOnlySpan<char>.Empty : input[keyRange];
var value = splitIdx < 1 ? input : input[(keyRange.End.Value + 1)..end];
ISearchQueryFilter res = key switch
{
"cw" => new CwFilter(negated, value.ToString()),
"from" or "author" or "by" or "user" => new FromFilter(negated, value.ToString()),
"mention" or "mentions" or "mentioning" => new MentionFilter(negated, value.ToString()),
"reply" or "replying" or "to" => new ReplyFilter(negated, value.ToString()),
"instance" or "domain" or "host" => new InstanceFilter(negated, value.ToString()),
"filter" when MiscFilter.TryParse(negated, value, out var parsed) => parsed,
"in" when InFilter.TryParse(negated, value, out var parsed) => parsed,
"visibility" when VisibilityFilter.TryParse(negated, value, out var parsed) => parsed,
"has" or "attachment" or "attached" when AttachmentFilter.TryParse(negated, value, out var parsed)
=> parsed,
"case" when CaseFilter.TryParse(value, out var parsed) => parsed,
"match" when MatchFilter.TryParse(value, out var parsed) => parsed,
"after" or "since" when DateOnly.TryParse(value, out var date) => new AfterFilter(date),
"before" or "until" when DateOnly.TryParse(value, out var date) => new BeforeFilter(date),
_ => new WordFilter(negated, input[pos..end].ToString())
};
pos = end;
return res;
}
}