diff --git a/src/SentenceStudio.Shared/Models/FilterToken.cs b/src/SentenceStudio.Shared/Models/FilterToken.cs index e859fefd..bca33fe3 100644 --- a/src/SentenceStudio.Shared/Models/FilterToken.cs +++ b/src/SentenceStudio.Shared/Models/FilterToken.cs @@ -10,7 +10,8 @@ public sealed record FilterToken(string Type, string Value) "status", "association", "language", - "encoding" + "encoding", + "type" }; public bool IsValid => diff --git a/src/SentenceStudio.Shared/Services/SearchQueryParser.cs b/src/SentenceStudio.Shared/Services/SearchQueryParser.cs index 8c28fb86..794ae80b 100644 --- a/src/SentenceStudio.Shared/Services/SearchQueryParser.cs +++ b/src/SentenceStudio.Shared/Services/SearchQueryParser.cs @@ -19,7 +19,7 @@ public class SearchQueryParser : ISearchQueryParser // Supports quoted values for multi-word filters: tag:"multi word" // Also supports unquoted values that end at whitespace private static readonly Regex FilterPattern = new( - @"(tag|resource|lemma|status|association|language|encoding):(?:""([^""]*)""|(\S+))", + @"(tag|resource|lemma|status|association|language|encoding|type):(?:""([^""]*)""|(\S+))", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// @@ -128,7 +128,7 @@ public bool IsValidFilterToken(string filterType, string filterValue) var textBeforeCursor = text.Substring(0, cursorPosition); // Match filter prefix at end of text: tag:partial or tag:"partial - var activeFilterPattern = new Regex(@"(tag|resource|lemma|status|association|language|encoding):(?:""([^""]*)|(\S*))$", RegexOptions.IgnoreCase); + var activeFilterPattern = new Regex(@"(tag|resource|lemma|status|association|language|encoding|type):(?:""([^""]*)|(\S*))$", RegexOptions.IgnoreCase); var match = activeFilterPattern.Match(textBeforeCursor); if (match.Success) diff --git a/tests/SentenceStudio.UnitTests/Services/SearchQueryParserTests.cs b/tests/SentenceStudio.UnitTests/Services/SearchQueryParserTests.cs index 2d95e84e..a06f9dcf 100644 --- a/tests/SentenceStudio.UnitTests/Services/SearchQueryParserTests.cs +++ b/tests/SentenceStudio.UnitTests/Services/SearchQueryParserTests.cs @@ -227,6 +227,35 @@ public void Parse_ComplexQuery_HandlesAllFilterTypes() result.FreeTextTerms.Should().Contain("단풍"); } + [Theory] + [InlineData("type:word", "word")] + [InlineData("type:phrase", "phrase")] + [InlineData("type:sentence", "sentence")] + [InlineData("TYPE:Sentence", "Sentence")] + public void Parse_TypeFilter_ExtractsCorrectly(string query, string expectedValue) + { + // Act + var result = _sut.Parse(query); + + // Assert + result.Filters.Should().HaveCount(1); + result.Filters[0].Type.Should().Be("type"); + result.Filters[0].Value.Should().Be(expectedValue); + } + + [Fact] + public void Parse_TypeFilterWithOtherFilters_ExtractsAll() + { + // Act + var result = _sut.Parse("type:sentence status:learning 단풍"); + + // Assert + result.Filters.Should().HaveCount(2); + result.Filters.Should().Contain(f => f.Type == "type" && f.Value == "sentence"); + result.Filters.Should().Contain(f => f.Type == "status" && f.Value == "learning"); + result.FreeTextTerms.Should().Contain("단풍"); + } + #endregion #region IsValidFilterToken Tests @@ -238,6 +267,9 @@ public void Parse_ComplexQuery_HandlesAllFilterTypes() [InlineData("status", "learning", true)] [InlineData("status", "known", true)] [InlineData("status", "unknown", true)] + [InlineData("type", "word", true)] + [InlineData("type", "phrase", true)] + [InlineData("type", "sentence", true)] [InlineData("status", "invalid", false)] [InlineData("invalid", "value", false)] [InlineData("tag", "", false)]