package web import ( "html/template" "regexp" "strings" "unicode/utf8" "github.com/gramanas/blogspace/internal/store" ) const ( searchPerPage = 20 maxSearchRunes = 100 // longer queries are cut; nobody types more on purpose snippetContext = 80 // runes kept on each side of the match ) // searchPattern turns what the reader typed into the regular expression both // Postgres (~*) and Go run: each word literal, the spaces between them "anything // in between", so "go tem" finds "Go templates". Blank → "". func searchPattern(q string) string { words := strings.Fields(q) for i, w := range words { words[i] = regexp.QuoteMeta(w) } return strings.Join(words, ".*") } // searchHit is one result: the post plus the excerpt the list shows. type searchHit struct { store.Post Snippet template.HTML } // searchSnippet is a short piece of the Markdown body around the first match, // with the match marked; when only the title matched it is the body's start. func searchSnippet(body string, re *regexp.Regexp) template.HTML { text := strings.Join(strings.Fields(body), " ") loc := re.FindStringIndex(text) if loc == nil { loc = []int{0, 0} } before, match, after := text[:loc[0]], text[loc[0]:loc[1]], text[loc[1]:] for n := utf8.RuneCountInString(before); n > snippetContext; n-- { // rune by rune: never cut inside one _, size := utf8.DecodeRuneInString(before) before = before[size:] } if len(before) < loc[0] { before = "…" + before } if utf8.RuneCountInString(after) > snippetContext { i := 0 for n := 0; n < snippetContext; n++ { _, size := utf8.DecodeRuneInString(after[i:]) i += size } after = after[:i] + "…" } out := template.HTMLEscapeString(before) if match != "" { out += "" + template.HTMLEscapeString(match) + "" } return template.HTML(out + template.HTMLEscapeString(after)) }