mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
/*
|
||
File Name: Text 2 Hash.go
|
||
Copyright: 2021 Peernet s.r.o.
|
||
Author: Peter Kleissner
|
||
*/
|
||
|
||
package search
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
|
||
"lukechampine.com/blake3"
|
||
)
|
||
|
||
// Words must be the minimum length to be indexed.
|
||
const wordMinLength = 3
|
||
|
||
// text2Hashes creates hashes from words in the text. Text may be CamelCased.
|
||
// Text must be already validated for valid UTF8 when calling this function.
|
||
func text2Hashes(text string, hashes map[[32]byte]string) {
|
||
words := strings.FieldsFunc(text, func(char rune) bool {
|
||
if unicode.IsSpace(char) {
|
||
return true
|
||
}
|
||
return strings.ContainsAny(string(char), "+-._()[],–")
|
||
})
|
||
|
||
for _, word := range words {
|
||
// remove hash tag prefix
|
||
word = strings.TrimPrefix(word, "#")
|
||
|
||
hashWordMap(word, hashes)
|
||
|
||
// CamelCase word detection
|
||
for _, word2 := range CamelCaseSplit(word) {
|
||
if word2 != word {
|
||
hashWordMap(word2, hashes)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// filename2Hashes creates hashes based on the filename and folder.
|
||
func filename2Hashes(filename, folder string, hashes map[[32]byte]string) {
|
||
if len(filename) < wordMinLength {
|
||
return
|
||
}
|
||
|
||
// First hash is created on the entire filename including extension. This is done for perfect matches.
|
||
hashWordMap(filename, hashes)
|
||
|
||
// Hash the filename without extension (and proceed without extension)
|
||
filename = filenameRemoveExtension(filename)
|
||
hashWordMap(filename, hashes)
|
||
|
||
// Hash each individual word of the filename and directory
|
||
text2Hashes(filename, hashes)
|
||
|
||
folder = strings.ReplaceAll(folder, "\\", " ")
|
||
folder = strings.ReplaceAll(folder, "/", " ")
|
||
text2Hashes(folder, hashes)
|
||
}
|
||
|
||
// hashWordMap hashes a word and stores it on the map. This immediately deduplicated hashes. It always lowercases the word.
|
||
func hashWordMap(word string, hashes map[[32]byte]string) {
|
||
word = strings.TrimSpace(strings.ToLower(word))
|
||
if len(word) < wordMinLength {
|
||
return
|
||
}
|
||
|
||
hashes[blake3.Sum256([]byte(word))] = word
|
||
}
|
||
|
||
// hashMapDelete deletes a hash from the list if the hash is valid
|
||
func hashMapDelete(hash []byte, hashes map[[32]byte]string) {
|
||
if len(hash) == 0 {
|
||
return
|
||
}
|
||
|
||
var hashB [32]byte
|
||
copy(hashB[:], hash)
|
||
|
||
delete(hashes, hashB)
|
||
}
|
||
|
||
// hashWord hashes a single word. It returns nil if not suitable. It always lowercases the word.
|
||
func hashWord(word string) (hash []byte, wordHashed string) {
|
||
word = strings.TrimSpace(strings.ToLower(word))
|
||
if len(word) < wordMinLength {
|
||
return
|
||
}
|
||
|
||
hashB := blake3.Sum256([]byte(word))
|
||
return hashB[:], word
|
||
}
|
||
|
||
// fork from https://github.com/fatih/camelcase/blob/master/camelcase.go
|
||
|
||
// Split splits the camelcase word and returns a list of words. It also
|
||
// supports digits. Both lower camel case and upper camel case are supported.
|
||
// For more info please check: http://en.wikipedia.org/wiki/CamelCase
|
||
//
|
||
// Examples
|
||
//
|
||
// "" => [""]
|
||
// "lowercase" => ["lowercase"]
|
||
// "Class" => ["Class"]
|
||
// "MyClass" => ["My", "Class"]
|
||
// "MyC" => ["My", "C"]
|
||
// "HTML" => ["HTML"]
|
||
// "PDFLoader" => ["PDF", "Loader"]
|
||
// "AString" => ["A", "String"]
|
||
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
|
||
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
|
||
// "GL11Version" => ["GL", "11", "Version"]
|
||
// "99Bottles" => ["99", "Bottles"]
|
||
// "May5" => ["May", "5"]
|
||
// "BFG9000" => ["BFG", "9000"]
|
||
// "BöseÜberraschung" => ["Böse", "Überraschung"]
|
||
// "Two spaces" => ["Two", " ", "spaces"]
|
||
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
|
||
//
|
||
// Splitting rules
|
||
//
|
||
// 1) If string is not valid UTF-8, return it without splitting as -> removed in this fork.
|
||
// single item array.
|
||
// 2) Assign all unicode characters into one of 4 sets: lower case
|
||
// letters, upper case letters, numbers, and all other characters.
|
||
// 3) Iterate through characters of string, introducing splits
|
||
// between adjacent characters that belong to different sets.
|
||
// 4) Iterate through array of split strings, and if a given string
|
||
// is upper case:
|
||
// if subsequent string is lower case:
|
||
// move last character of upper case string to beginning of
|
||
// lower case string
|
||
func CamelCaseSplit(src string) (entries []string) {
|
||
entries = []string{}
|
||
var runes [][]rune
|
||
lastClass := 0
|
||
class := 0
|
||
// split into fields based on class of unicode character
|
||
for _, r := range src {
|
||
switch true {
|
||
case unicode.IsLower(r):
|
||
class = 1
|
||
case unicode.IsUpper(r):
|
||
class = 2
|
||
case unicode.IsDigit(r):
|
||
class = 3
|
||
default:
|
||
class = 4
|
||
}
|
||
if class == lastClass {
|
||
runes[len(runes)-1] = append(runes[len(runes)-1], r)
|
||
} else {
|
||
runes = append(runes, []rune{r})
|
||
}
|
||
lastClass = class
|
||
}
|
||
// handle upper case -> lower case sequences, e.g.
|
||
// "PDFL", "oader" -> "PDF", "Loader"
|
||
for i := 0; i < len(runes)-1; i++ {
|
||
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
|
||
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
|
||
runes[i] = runes[i][:len(runes[i])-1]
|
||
}
|
||
}
|
||
// construct []string from results
|
||
for _, s := range runes {
|
||
if len(s) > 0 {
|
||
entries = append(entries, string(s))
|
||
}
|
||
}
|
||
return
|
||
}
|