mirror of https://github.com/stashapp/stash.git
168 lines
4.0 KiB
Go
168 lines
4.0 KiB
Go
package models
|
|
|
|
import "strings"
|
|
|
|
const (
|
|
or = "OR"
|
|
orSymbol = "|"
|
|
notPrefix = '-'
|
|
phraseChar = '"'
|
|
)
|
|
|
|
// SearchSpecs provides the specifications for text-based searches.
|
|
type SearchSpecs struct {
|
|
// MustHave specifies all of the terms that must appear in the results.
|
|
MustHave []string
|
|
|
|
// AnySets specifies sets of terms where one of each set must appear in the results.
|
|
AnySets [][]string
|
|
|
|
// MustNot specifies all terms that must not appear in the results.
|
|
MustNot []string
|
|
}
|
|
|
|
// combinePhrases detects quote characters at the start and end of
|
|
// words and combines the contents into a single word.
|
|
func combinePhrases(words []string) []string {
|
|
var ret []string
|
|
startIndex := -1
|
|
for i, w := range words {
|
|
if startIndex == -1 {
|
|
// looking for start of phrase
|
|
// this could either be " or -"
|
|
ww := w
|
|
if len(w) > 0 && w[0] == notPrefix {
|
|
ww = w[1:]
|
|
}
|
|
if len(ww) > 0 && ww[0] == phraseChar && (len(ww) < 2 || ww[len(ww)-1] != phraseChar) {
|
|
startIndex = i
|
|
continue
|
|
}
|
|
|
|
ret = append(ret, w)
|
|
} else if len(w) > 0 && w[len(w)-1] == phraseChar { // looking for end of phrase
|
|
// combine words
|
|
phrase := strings.Join(words[startIndex:i+1], " ")
|
|
|
|
// add to return value
|
|
ret = append(ret, phrase)
|
|
startIndex = -1
|
|
}
|
|
}
|
|
|
|
if startIndex != -1 {
|
|
ret = append(ret, words[startIndex:]...)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func extractOrConditions(words []string, searchSpec *SearchSpecs) []string {
|
|
for foundOr := true; foundOr; {
|
|
foundOr = false
|
|
for i, w := range words {
|
|
if i > 0 && i < len(words)-1 && (strings.EqualFold(w, or) || w == orSymbol) {
|
|
// found an OR keyword
|
|
// first operand will be the last word
|
|
startIndex := i - 1
|
|
|
|
// find the last operand
|
|
// this will be the last word not preceded by OR
|
|
lastIndex := len(words) - 1
|
|
for ii := i + 2; ii < len(words); ii += 2 {
|
|
if !strings.EqualFold(words[ii], or) {
|
|
lastIndex = ii - 1
|
|
break
|
|
}
|
|
}
|
|
|
|
foundOr = true
|
|
|
|
// combine the words into an any set
|
|
var set []string
|
|
for ii := startIndex; ii <= lastIndex; ii += 2 {
|
|
word := extractPhrase(words[ii])
|
|
if word == "" {
|
|
continue
|
|
}
|
|
set = append(set, word)
|
|
}
|
|
|
|
searchSpec.AnySets = append(searchSpec.AnySets, set)
|
|
|
|
// take out the OR'd words
|
|
words = append(words[0:startIndex], words[lastIndex+1:]...)
|
|
|
|
// break and reparse
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return words
|
|
}
|
|
|
|
func extractNotConditions(words []string, searchSpec *SearchSpecs) []string {
|
|
var ret []string
|
|
|
|
for _, w := range words {
|
|
if len(w) > 1 && w[0] == notPrefix {
|
|
word := extractPhrase(w[1:])
|
|
if word == "" {
|
|
continue
|
|
}
|
|
searchSpec.MustNot = append(searchSpec.MustNot, word)
|
|
} else {
|
|
ret = append(ret, w)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func extractPhrase(w string) string {
|
|
if len(w) > 1 && w[0] == phraseChar && w[len(w)-1] == phraseChar {
|
|
return w[1 : len(w)-1]
|
|
}
|
|
|
|
return w
|
|
}
|
|
|
|
// ParseSearchString parses the Q value and returns a SearchSpecs object.
|
|
//
|
|
// By default, any words in the search value must appear in the results.
|
|
// Words encompassed by quotes (") as treated as a single term.
|
|
// Where keyword "OR" (case-insensitive) appears (and is not part of a quoted phrase), one of the
|
|
// OR'd terms must appear in the results.
|
|
// Where a keyword is prefixed with "-", that keyword must not appear in the results.
|
|
// Where OR appears as the first or last term, or where one of the OR operands has a
|
|
// not prefix, then the OR is treated literally.
|
|
func ParseSearchString(s string) SearchSpecs {
|
|
s = strings.TrimSpace(s)
|
|
|
|
if s == "" {
|
|
return SearchSpecs{}
|
|
}
|
|
|
|
// break into words
|
|
words := strings.Split(s, " ")
|
|
|
|
// combine phrases first, then extract OR conditions, then extract NOT conditions
|
|
// and the leftovers will be AND'd
|
|
ret := SearchSpecs{}
|
|
words = combinePhrases(words)
|
|
words = extractOrConditions(words, &ret)
|
|
words = extractNotConditions(words, &ret)
|
|
|
|
for _, w := range words {
|
|
// ignore empty quotes
|
|
word := extractPhrase(w)
|
|
if word == "" {
|
|
continue
|
|
}
|
|
ret.MustHave = append(ret.MustHave, word)
|
|
}
|
|
|
|
return ret
|
|
}
|