Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
brandPrefixRe = regexp.MustCompile(`(?i)^([A-Za-z][A-Za-z0-9&.\-]{1,40})\b`)
|
||||
dimTripleRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
|
||||
dimPairRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
|
||||
weightRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*(kg|g|lb|oz)\b`)
|
||||
)
|
||||
|
||||
// FillMissingFields derives sensible standard fields from name/attrs when absent.
|
||||
func FillMissingFields(mapped map[string]any, attrs map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(mapped)+8)
|
||||
for k, v := range mapped {
|
||||
out[k] = v
|
||||
}
|
||||
name := stringFromAny(out["name"])
|
||||
if name == "" {
|
||||
name = stringFromAny(out["title"])
|
||||
}
|
||||
|
||||
if stringFromAny(out["brand"]) == "" {
|
||||
if b := stringFromAny(attrs["brand"]); b != "" {
|
||||
out["brand"] = b
|
||||
} else if b := inferBrand(name); b != "" {
|
||||
out["brand"] = b
|
||||
}
|
||||
}
|
||||
|
||||
if stringFromAny(out["gtin"]) == "" {
|
||||
if g := stringFromAny(out["ean"]); g != "" {
|
||||
out["gtin"] = g
|
||||
}
|
||||
}
|
||||
|
||||
blob := name + " " + stringFromAny(out["description"])
|
||||
for _, m := range []map[string]any{attrs, out} {
|
||||
for _, k := range []string{"dimensions", "size", "dimension"} {
|
||||
blob += " " + stringFromAny(m[k])
|
||||
}
|
||||
}
|
||||
|
||||
if stringFromAny(out["width"]) == "" || stringFromAny(out["height"]) == "" || stringFromAny(out["depth"]) == "" {
|
||||
if w, h, d, ok := parseDimensions(blob); ok {
|
||||
if stringFromAny(out["width"]) == "" {
|
||||
out["width"] = w
|
||||
}
|
||||
if stringFromAny(out["height"]) == "" {
|
||||
out["height"] = h
|
||||
}
|
||||
if stringFromAny(out["depth"]) == "" && d != "" {
|
||||
out["depth"] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stringFromAny(out["weight"]) == "" {
|
||||
if w := stringFromAny(attrs["weight"]); w != "" {
|
||||
out["weight"] = w
|
||||
} else if w, ok := parseWeight(blob); ok {
|
||||
out["weight"] = w
|
||||
}
|
||||
}
|
||||
|
||||
if stringFromAny(out["category"]) == "" {
|
||||
if c := stringFromAny(attrs["category"]); c != "" {
|
||||
out["category"] = c
|
||||
}
|
||||
}
|
||||
|
||||
if stringFromAny(out["stock_status"]) == "" {
|
||||
if s := stringFromAny(out["stock"]); s != "" {
|
||||
out["stock_status"] = normalizeStockStatusLabel(s)
|
||||
}
|
||||
} else {
|
||||
out["stock_status"] = normalizeStockStatusLabel(stringFromAny(out["stock_status"]))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func inferBrand(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
m := brandPrefixRe.FindStringSubmatch(name)
|
||||
if len(m) < 2 {
|
||||
return ""
|
||||
}
|
||||
b := strings.TrimSpace(m[1])
|
||||
// Skip generic leading words
|
||||
switch strings.ToLower(b) {
|
||||
case "the", "new", "set", "pack", "pair", "product", "item":
|
||||
return ""
|
||||
}
|
||||
return SanitizeOutput(b)
|
||||
}
|
||||
|
||||
func parseDimensions(blob string) (w, h, d string, ok bool) {
|
||||
if m := dimTripleRe.FindStringSubmatch(blob); len(m) == 4 {
|
||||
return normalizeNum(m[1]), normalizeNum(m[2]), normalizeNum(m[3]), true
|
||||
}
|
||||
if m := dimPairRe.FindStringSubmatch(blob); len(m) == 3 {
|
||||
return normalizeNum(m[1]), normalizeNum(m[2]), "", true
|
||||
}
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
func parseWeight(blob string) (string, bool) {
|
||||
m := weightRe.FindStringSubmatch(blob)
|
||||
if len(m) < 3 {
|
||||
return "", false
|
||||
}
|
||||
return normalizeNum(m[1]) + " " + strings.ToLower(m[2]), true
|
||||
}
|
||||
|
||||
func normalizeNum(s string) string {
|
||||
return strings.ReplaceAll(strings.TrimSpace(s), ",", ".")
|
||||
}
|
||||
|
||||
func normalizeStockStatusLabel(s string) string {
|
||||
if mapped := MapStockStatus(s); mapped != "" {
|
||||
return mapped
|
||||
}
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
switch {
|
||||
case s == "" || s == "0" || strings.Contains(s, "out"):
|
||||
return "out_of_stock"
|
||||
case strings.Contains(s, "pre"):
|
||||
return "preorder"
|
||||
case strings.Contains(s, "back"):
|
||||
return "backorder"
|
||||
default:
|
||||
return "in_stock"
|
||||
}
|
||||
}
|
||||
|
||||
func stringFromAny(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case float64, float32, int, int64, bool:
|
||||
s := strings.TrimSpace(fmt.Sprint(t))
|
||||
if s == "<nil>" {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user