diff --git a/domain/src/usecases/search/SearchTermsParser.kt b/domain/src/usecases/search/SearchTermsParser.kt index 9d0cc95..cbc9a64 100644 --- a/domain/src/usecases/search/SearchTermsParser.kt +++ b/domain/src/usecases/search/SearchTermsParser.kt @@ -1,38 +1,91 @@ package be.simplenotes.domain.usecases.search import be.simplenotes.search.SearchTerms +import java.util.* -private fun innerRegex(name: String) = - """$name:['"](.*?)['"]""".toRegex() -private fun outerRegex(name: String) = - """($name:['"].*?['"])""".toRegex() +private enum class Quote { SingleQuote, DoubleQuote, } -private val titleRe = innerRegex("title") -private val outerTitleRe = outerRegex("title") +data class ParsedSearchInput(val global: List, val entries: Map) -private val tagRe = innerRegex("tag") -private val outerTagRe = outerRegex("tag") +object SearchInputParser { + fun parseInput(input: String): ParsedSearchInput { + val tokenizer = StringTokenizer(input, ":\"' ", true) -private val contentRe = innerRegex("content") -private val outerContentRe = outerRegex("content") + val tokens = ArrayList() + val current = StringBuilder() + var quoteOpen: Quote? = null + + fun push() { + if (current.isNotEmpty()) { + tokens.add(current.toString()) + } + current.setLength(0) + quoteOpen = null + } + + while (tokenizer.hasMoreTokens()) { + when (val token = tokenizer.nextToken()) { + "\"" -> when { + Quote.DoubleQuote == quoteOpen -> push() + quoteOpen == null -> quoteOpen = Quote.DoubleQuote + else -> current.append(token) + } + "'" -> when { + Quote.SingleQuote == quoteOpen -> push() + quoteOpen == null -> quoteOpen = Quote.SingleQuote + else -> current.append(token) + } + " " -> { + if (quoteOpen != null) current.append(" ") + else push() + } + ":" -> { + push() + tokens.add(token) + } + else -> { + current.append(token) + } + } + } + + push() + + val entries = HashMap() + + val colonIndexes = ArrayList() + tokens.forEachIndexed { index, token -> + if (token == ":") colonIndexes += index + } + + var changes = 0 + for (colonIndex in colonIndexes) { + val offset = changes * 3 + + val key = tokens.getOrNull(colonIndex - 1 - offset) + val value = tokens.getOrNull(colonIndex + 1 - offset) + + if (key != null && value != null) { + entries[key] = value + tokens.removeAt(colonIndex - 1 - offset) // remove key + tokens.removeAt(colonIndex - 1 - offset) // remove : + tokens.removeAt(colonIndex - 1 - offset) // remove value + changes++ + } + } + + return ParsedSearchInput(global = tokens, entries = entries) + } +} internal fun parseSearchTerms(input: String): SearchTerms { - var c: String = input + val parsedInput = SearchInputParser.parseInput(input) - fun extract(innerRegex: Regex, outerRegex: Regex): String? { - val match = innerRegex.find(input)?.groups?.get(1)?.value - if (match != null) { - val group = outerRegex.find(input)?.groups?.get(1)?.value - group?.let { c = c.replace(it, "") } - } - return match - } + val title: String? = parsedInput.entries["title"] + val tag: String? = parsedInput.entries["tag"] + val content: String? = parsedInput.entries["content"] - val title: String? = extract(titleRe, outerTitleRe) - val tag: String? = extract(tagRe, outerTagRe) - val content: String? = extract(contentRe, outerContentRe) - - val all = c.trim().ifEmpty { null } + val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ") return SearchTerms( title = title, diff --git a/domain/test/usecases/search/SearchTermsParserKtTest.kt b/domain/test/usecases/search/SearchTermsParserKtTest.kt index 7d4509f..85da907 100644 --- a/domain/test/usecases/search/SearchTermsParserKtTest.kt +++ b/domain/test/usecases/search/SearchTermsParserKtTest.kt @@ -35,6 +35,14 @@ internal class SearchTermsParserKtTest { tag = "example abc", all = "this is the end" ), + createResult("tag:blah", tag = "blah"), + createResult("tag:'some words'", tag = "some words"), + createResult("tag:'some words ' global", tag = "some words ", all = "global"), + createResult( + "tag:'double quote inside single \" ' global", + tag = "double quote inside single \" ", + all = "global" + ), ) @ParameterizedTest