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:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+160
View File
@@ -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 ""
}
}