270 lines
6.5 KiB
Go
270 lines
6.5 KiB
Go
package processing
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// knownAttrKeyAliases maps compact feed/spec tokens onto compact category
|
|
// attribute_key tokens (Slovenian catalog keys + common English synonyms).
|
|
var knownAttrKeyAliases = map[string]string{
|
|
"velikostzaslona": "diagonalazaslona",
|
|
"screensize": "diagonalazaslona",
|
|
"screen": "diagonalazaslona",
|
|
"diagonal": "diagonalazaslona",
|
|
"diagonala": "diagonalazaslona",
|
|
"nastavljivpovisini": "nastavljivopovisini",
|
|
"heightadjustable": "nastavljivopovisini",
|
|
"zaslonnadotik": "zaslonobcutljivnadotik",
|
|
"touchscreen": "zaslonobcutljivnadotik",
|
|
"tipmatrike": "vrstapanela",
|
|
"tippanela": "vrstapanela",
|
|
"paneltype": "vrstapanela",
|
|
"panel": "vrstapanela",
|
|
"zvocniki": "vgrajenizvocniki",
|
|
"speakers": "vgrajenizvocniki",
|
|
"ukrivljenzaslon": "oblikazaslona",
|
|
"curved": "oblikazaslona",
|
|
"slusalkezmikrofonom": "vgrajenmikrofon",
|
|
"mikrofon": "vgrajenmikrofon",
|
|
"builtinmic": "vgrajenmikrofon",
|
|
"microphone": "vgrajenmikrofon",
|
|
"brezzicne": "brezzicnapovezava",
|
|
"brezzicna": "brezzicnapovezava",
|
|
"wireless": "brezzicnapovezava",
|
|
"bluetooth": "brezzicnatehnologija",
|
|
"bt": "brezzicnatehnologija",
|
|
"anc": "odpravljanjehrupa",
|
|
"noisecancelling": "odpravljanjehrupa",
|
|
"noisecanceling": "odpravljanjehrupa",
|
|
"odpravljanjehrupa": "odpravljanjehrupa",
|
|
"resolution": "locljivost",
|
|
"responsetime": "odzivnicas",
|
|
}
|
|
|
|
// MapAttrsOntoAllowedKeys remaps feed/spec keys onto category_attributes keys
|
|
// when a confident match exists (exact compact, known alias, or unique token
|
|
// subset). Existing allowed/core values are never overwritten. When allowed is
|
|
// nil or empty, attrs are returned unchanged (caller still filters).
|
|
func MapAttrsOntoAllowedKeys(attrs map[string]any, allowed map[string]struct{}) map[string]any {
|
|
if len(attrs) == 0 || len(allowed) == 0 {
|
|
return attrs
|
|
}
|
|
|
|
prefer := preferredAllowedKeys(allowed)
|
|
if len(prefer) == 0 {
|
|
return attrs
|
|
}
|
|
|
|
out := make(map[string]any, len(attrs)+len(prefer))
|
|
for k, v := range attrs {
|
|
out[k] = v
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
if !attrValuePresent(v) {
|
|
continue
|
|
}
|
|
canon := canonicalizeAttrKey(k)
|
|
if canon != "" {
|
|
if _, ok := coreCharacteristicAttrKeys[canon]; ok {
|
|
continue
|
|
}
|
|
if _, ok := allowed[canon]; ok {
|
|
continue
|
|
}
|
|
if _, ok := allowed[strings.ToLower(strings.TrimSpace(k))]; ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
target := resolveAllowedAttrKey(k, prefer)
|
|
if target == "" || target == canon || target == k {
|
|
continue
|
|
}
|
|
if attrValuePresent(out[target]) {
|
|
continue
|
|
}
|
|
out[target] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func preferredAllowedKeys(allowed map[string]struct{}) map[string]string {
|
|
// compact → preferred output key (canonical snake_case when possible)
|
|
prefer := make(map[string]string, len(allowed))
|
|
for k := range allowed {
|
|
c := compactAttrToken(k)
|
|
if c == "" {
|
|
continue
|
|
}
|
|
canon := canonicalizeAttrKey(k)
|
|
if canon == "" {
|
|
canon = strings.ToLower(strings.TrimSpace(k))
|
|
}
|
|
if prev, ok := prefer[c]; ok {
|
|
// Prefer underscore form over hyphen duplicates from loadCategoryAttributeKeySets.
|
|
if strings.Contains(canon, "_") && !strings.Contains(prev, "_") {
|
|
prefer[c] = canon
|
|
}
|
|
continue
|
|
}
|
|
prefer[c] = canon
|
|
}
|
|
return prefer
|
|
}
|
|
|
|
func resolveAllowedAttrKey(feedKey string, prefer map[string]string) string {
|
|
ck := compactAttrToken(feedKey)
|
|
if ck == "" {
|
|
return ""
|
|
}
|
|
if target, ok := prefer[ck]; ok {
|
|
return target
|
|
}
|
|
if alias, ok := knownAttrKeyAliases[ck]; ok {
|
|
if target, ok := prefer[alias]; ok {
|
|
return target
|
|
}
|
|
}
|
|
return softMatchAllowedKey(feedKey, ck, prefer)
|
|
}
|
|
|
|
func softMatchAllowedKey(feedKey, feedCompact string, prefer map[string]string) string {
|
|
feedTokens := attrKeyTokens(feedKey)
|
|
if len(feedTokens) == 0 && feedCompact == "" {
|
|
return ""
|
|
}
|
|
|
|
var hits []string
|
|
for compact, target := range prefer {
|
|
if feedCompact != "" && (strings.Contains(compact, feedCompact) || strings.Contains(feedCompact, compact)) {
|
|
// Require meaningful overlap (avoid tiny tokens matching everything).
|
|
shorter, longer := feedCompact, compact
|
|
if len(shorter) > len(longer) {
|
|
shorter, longer = longer, shorter
|
|
}
|
|
if len(shorter) >= 6 && len(shorter)*10 >= len(longer)*6 {
|
|
hits = append(hits, target)
|
|
continue
|
|
}
|
|
}
|
|
if len(feedTokens) == 0 {
|
|
continue
|
|
}
|
|
allowedTokens := attrKeyTokens(target)
|
|
if len(allowedTokens) == 0 {
|
|
allowedTokens = attrKeyTokens(compact)
|
|
}
|
|
if tokensSubsetMatch(feedTokens, allowedTokens) {
|
|
hits = append(hits, target)
|
|
}
|
|
}
|
|
if len(hits) != 1 {
|
|
return ""
|
|
}
|
|
return hits[0]
|
|
}
|
|
|
|
func tokensSubsetMatch(feed, allowed []string) bool {
|
|
significant := 0
|
|
for _, ft := range feed {
|
|
if len(ft) < 3 {
|
|
continue
|
|
}
|
|
significant++
|
|
ok := false
|
|
for _, at := range allowed {
|
|
if at == ft || strings.HasPrefix(at, ft) || strings.HasPrefix(ft, at) {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
return significant >= 2
|
|
}
|
|
|
|
func attrKeyTokens(k string) []string {
|
|
k = strings.ToLower(strings.TrimSpace(k))
|
|
if k == "" {
|
|
return nil
|
|
}
|
|
var b strings.Builder
|
|
var out []string
|
|
flush := func() {
|
|
if b.Len() == 0 {
|
|
return
|
|
}
|
|
tok := b.String()
|
|
b.Reset()
|
|
tok = compactAttrToken(tok)
|
|
if len(tok) >= 2 {
|
|
out = append(out, tok)
|
|
}
|
|
}
|
|
for _, r := range k {
|
|
r = foldAttrRune(r)
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
|
b.WriteRune(r)
|
|
default:
|
|
flush()
|
|
}
|
|
}
|
|
flush()
|
|
return out
|
|
}
|
|
|
|
func compactAttrToken(k string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(k))
|
|
for _, r := range strings.ToLower(k) {
|
|
r = foldAttrRune(r)
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func foldAttrRune(r rune) rune {
|
|
switch r {
|
|
case 'š', 'ś', 'ş':
|
|
return 's'
|
|
case 'č', 'ć', 'ç':
|
|
return 'c'
|
|
case 'ž', 'ź', 'ż':
|
|
return 'z'
|
|
case 'đ':
|
|
return 'd'
|
|
case 'ä', 'á', 'à', 'â', 'ã', 'å':
|
|
return 'a'
|
|
case 'ë', 'é', 'è', 'ê':
|
|
return 'e'
|
|
case 'ï', 'í', 'ì', 'î':
|
|
return 'i'
|
|
case 'ö', 'ó', 'ò', 'ô', 'õ':
|
|
return 'o'
|
|
case 'ü', 'ú', 'ù', 'û':
|
|
return 'u'
|
|
case 'ý', 'ÿ':
|
|
return 'y'
|
|
}
|
|
if unicode.IsLetter(r) {
|
|
return unicode.ToLower(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func attrValuePresent(v any) bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
s := strings.TrimSpace(fmt.Sprint(v))
|
|
return s != "" && s != "<nil>"
|
|
}
|