239 lines
6 KiB
Go
239 lines
6 KiB
Go
package catalog
|
||
|
||
import (
|
||
"fmt"
|
||
"rubric/modules/devopsmodules"
|
||
"rubric/modules/gomodules"
|
||
"rubric/modules/infosecmodules"
|
||
"rubric/modules/linuxmodules"
|
||
"rubric/utils"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// SearchResult — одно совпадение поиска
|
||
type SearchResult struct {
|
||
BookKey string
|
||
BookTitle string
|
||
ModKey string
|
||
ModTitle string
|
||
ModDesc string
|
||
RunFunc func()
|
||
Score int // количество совпадений
|
||
}
|
||
|
||
// searchEntry — унифицированная запись для поиска
|
||
type searchEntry struct {
|
||
bookKey string
|
||
bookTitle string
|
||
modKey string
|
||
modTitle string
|
||
modDesc string
|
||
keywords []string
|
||
run func()
|
||
}
|
||
|
||
// Все записи для поиска — строим один раз
|
||
var allEntries = buildSearchIndex()
|
||
|
||
func buildSearchIndex() []searchEntry {
|
||
entries := []searchEntry{}
|
||
|
||
// Go
|
||
for _, m := range gomodules.All {
|
||
entries = append(entries, searchEntry{
|
||
bookKey: "go", bookTitle: "Go",
|
||
modKey: m.Key, modTitle: m.Title, modDesc: m.Description,
|
||
keywords: gomodules.Keywords(m.Key),
|
||
run: m.Run,
|
||
})
|
||
}
|
||
|
||
// Linux
|
||
for _, m := range linuxmodules.All {
|
||
entries = append(entries, searchEntry{
|
||
bookKey: "linux", bookTitle: "Linux",
|
||
modKey: m.Key, modTitle: m.Title, modDesc: m.Description,
|
||
keywords: linuxmodules.Keywords(m.Key),
|
||
run: m.Run,
|
||
})
|
||
}
|
||
|
||
// DevOps
|
||
for _, m := range devopsmodules.All {
|
||
entries = append(entries, searchEntry{
|
||
bookKey: "devops", bookTitle: "DevOps",
|
||
modKey: m.Key, modTitle: m.Title, modDesc: m.Description,
|
||
keywords: devopsmodules.Keywords(m.Key),
|
||
run: m.Run,
|
||
})
|
||
}
|
||
|
||
// InfoSec
|
||
for _, m := range infosecmodules.All {
|
||
entries = append(entries, searchEntry{
|
||
bookKey: "infosec", bookTitle: "InfoSec",
|
||
modKey: m.Key, modTitle: m.Title, modDesc: m.Description,
|
||
keywords: infosecmodules.Keywords(m.Key),
|
||
run: m.Run,
|
||
})
|
||
}
|
||
|
||
return entries
|
||
}
|
||
|
||
// Search выполняет поиск по запросу, возвращает отсортированные результаты
|
||
func Search(query string) []SearchResult {
|
||
query = strings.ToLower(strings.TrimSpace(query))
|
||
if query == "" {
|
||
return nil
|
||
}
|
||
|
||
// Разбить запрос на слова
|
||
words := strings.Fields(query)
|
||
|
||
var results []SearchResult
|
||
|
||
for _, e := range allEntries {
|
||
score := 0
|
||
|
||
// Текст для поиска: ключевые слова + название + описание
|
||
searchText := strings.ToLower(
|
||
e.modKey + " " +
|
||
e.modTitle + " " +
|
||
e.modDesc + " " +
|
||
strings.Join(e.keywords, " "),
|
||
)
|
||
|
||
for _, word := range words {
|
||
if len(word) < 2 {
|
||
continue
|
||
}
|
||
// Точное совпадение в ключевых словах — больший вес
|
||
for _, kw := range e.keywords {
|
||
if strings.EqualFold(kw, word) {
|
||
score += 10
|
||
} else if strings.Contains(strings.ToLower(kw), word) {
|
||
score += 5
|
||
}
|
||
}
|
||
// Совпадение в ключе модуля
|
||
if strings.Contains(e.modKey, word) {
|
||
score += 8
|
||
}
|
||
// Совпадение в названии
|
||
if strings.Contains(strings.ToLower(e.modTitle), word) {
|
||
score += 6
|
||
}
|
||
// Совпадение в описании
|
||
if strings.Contains(strings.ToLower(e.modDesc), word) {
|
||
score += 3
|
||
}
|
||
// Совпадение в общем тексте
|
||
if strings.Contains(searchText, word) && score == 0 {
|
||
score += 1
|
||
}
|
||
}
|
||
|
||
if score > 0 {
|
||
results = append(results, SearchResult{
|
||
BookKey: e.bookKey,
|
||
BookTitle: e.bookTitle,
|
||
ModKey: e.modKey,
|
||
ModTitle: e.modTitle,
|
||
ModDesc: e.modDesc,
|
||
RunFunc: e.run,
|
||
Score: score,
|
||
})
|
||
}
|
||
}
|
||
|
||
// Сортировать по релевантности
|
||
sort.Slice(results, func(i, j int) bool {
|
||
return results[i].Score > results[j].Score
|
||
})
|
||
|
||
return results
|
||
}
|
||
|
||
// ShowSearchResults выводит результаты поиска
|
||
func ShowSearchResults(query string, results []SearchResult) {
|
||
fmt.Printf("\n%s%s🔍 Поиск: \"%s\"%s\n\n",
|
||
utils.ColorBold, utils.ColorCyan, query, utils.ColorReset)
|
||
|
||
if len(results) == 0 {
|
||
fmt.Printf(" %sНичего не найдено по запросу \"%s\"%s\n\n",
|
||
utils.ColorDim, query, utils.ColorReset)
|
||
fmt.Printf(" Попробуйте другие слова. Примеры: ansible, nmap, docker, jwt, selinux\n\n")
|
||
return
|
||
}
|
||
|
||
fmt.Printf(" %s%sНайдено разделов: %d%s\n\n",
|
||
utils.ColorBold, utils.ColorGreen, len(results), utils.ColorReset)
|
||
|
||
for i, r := range results {
|
||
bookColor := bookColorFor(r.BookKey)
|
||
fmt.Printf(" %s%s%2d.%s %s[%s]%s %s%-12s%s — %s%s%s\n",
|
||
utils.ColorBold, utils.ColorCyan, i+1, utils.ColorReset,
|
||
utils.ColorBold+bookColor, r.BookTitle, utils.ColorReset,
|
||
utils.ColorYellow+utils.ColorBold, r.ModKey, utils.ColorReset,
|
||
utils.ColorWhite, r.ModTitle, utils.ColorReset,
|
||
)
|
||
fmt.Printf(" %s%s%s\n",
|
||
utils.ColorDim, r.ModDesc, utils.ColorReset)
|
||
fmt.Printf(" %sОткрыть: ./rubric -book %s -topic %s%s\n\n",
|
||
utils.ColorDim, r.BookKey, r.ModKey, utils.ColorReset)
|
||
}
|
||
}
|
||
|
||
func bookColorFor(key string) string {
|
||
switch key {
|
||
case "go":
|
||
return utils.ColorCyan
|
||
case "linux":
|
||
return utils.ColorGreen
|
||
case "devops":
|
||
return utils.ColorMagenta
|
||
case "infosec":
|
||
return utils.ColorRed
|
||
}
|
||
return utils.ColorWhite
|
||
}
|
||
|
||
// InteractiveSearch — интерактивный поиск с возможностью открыть раздел
|
||
func InteractiveSearch(query string, scanner interface {
|
||
Scan() bool
|
||
Text() string
|
||
}) {
|
||
results := Search(query)
|
||
ShowSearchResults(query, results)
|
||
|
||
if len(results) == 0 {
|
||
return
|
||
}
|
||
|
||
fmt.Printf(" %s%sВведите номер раздела чтобы открыть (Enter — пропустить):%s ",
|
||
utils.ColorBold, utils.ColorGreen, utils.ColorReset)
|
||
|
||
if !scanner.Scan() {
|
||
return
|
||
}
|
||
input := strings.TrimSpace(scanner.Text())
|
||
if input == "" {
|
||
return
|
||
}
|
||
|
||
n := 0
|
||
for _, ch := range input {
|
||
if ch >= '0' && ch <= '9' {
|
||
n = n*10 + int(ch-'0')
|
||
} else {
|
||
return
|
||
}
|
||
}
|
||
|
||
if n >= 1 && n <= len(results) {
|
||
fmt.Println()
|
||
results[n-1].RunFunc()
|
||
}
|
||
}
|