package processing import ( "fmt" "strings" ) // isDescriptionFieldKey reports keys whose values should be plain text scalars // (not JSON arrays / nested HTML wrappers) for process + V1 poll contracts. func isDescriptionFieldKey(key string) bool { switch strings.ToLower(strings.TrimSpace(key)) { case "description", "desc", "body", "short_description", "shortdescription", "processed_description", "long_description", "longdescription": return true default: return false } } // PlainDescriptionFromAny coerces legacy description shapes to a single plain // string: string, {#text}, or arrays of those (joined with newlines). HTML is // stripped via v1PlainDescription — use for meta/SEO and feed normalize only. // V1 process poll / formula bodies use DescriptionFromAny (keeps markup). func PlainDescriptionFromAny(v any) string { if v == nil { return "" } switch t := v.(type) { case string: return v1PlainDescription(t) case []string: parts := make([]string, 0, len(t)) for _, s := range t { if p := v1PlainDescription(s); p != "" { parts = append(parts, p) } } return strings.Join(parts, "\n") case []any: parts := make([]string, 0, len(t)) for _, item := range t { if p := PlainDescriptionFromAny(item); p != "" { parts = append(parts, p) } } return strings.Join(parts, "\n") case map[string]any: for _, k := range []string{"#text", "text", "value", "description", "body"} { if p := PlainDescriptionFromAny(t[k]); p != "" { return p } } return "" default: s := strings.TrimSpace(fmt.Sprint(t)) if s == "" || s == "" { return "" } return v1PlainDescription(s) } } // DescriptionFromAny coerces legacy description shapes to a single string while // preserving formula HTML (h1/h2/p/ul/…). Unwraps JSON arrays / #text wrappers // but does not strip tags — used by V1 process item projection and catalog // normalize when enhance/formula HTML must reach clients and stay in DB. func DescriptionFromAny(v any) string { if v == nil { return "" } switch t := v.(type) { case string: return v1PreserveDescription(t) case []string: parts := make([]string, 0, len(t)) for _, s := range t { if p := v1PreserveDescription(s); p != "" { parts = append(parts, p) } } return strings.Join(parts, "\n") case []any: parts := make([]string, 0, len(t)) for _, item := range t { if p := DescriptionFromAny(item); p != "" { parts = append(parts, p) } } return strings.Join(parts, "\n") case map[string]any: for _, k := range []string{"#text", "text", "value", "description", "body"} { if p := DescriptionFromAny(t[k]); p != "" { return p } } return "" default: s := strings.TrimSpace(fmt.Sprint(t)) if s == "" || s == "" { return "" } return v1PreserveDescription(s) } } // coerceScalarText flattens arrays / #text wrappers into a trimmed string without // HTML stripping (names, brands, stock labels, …). func coerceScalarText(v any) string { if v == nil { return "" } switch t := v.(type) { case string: return strings.TrimSpace(t) case []string: parts := make([]string, 0, len(t)) for _, s := range t { s = strings.TrimSpace(s) if s != "" { parts = append(parts, s) } } return strings.Join(parts, " ") case []any: parts := make([]string, 0, len(t)) for _, item := range t { if s := coerceScalarText(item); s != "" { parts = append(parts, s) } } return strings.Join(parts, " ") case map[string]any: for _, k := range []string{"#text", "text", "value"} { if s := coerceScalarText(t[k]); s != "" { return s } } return "" case float64, float32, int, int64, int32, bool: s := strings.TrimSpace(fmt.Sprint(t)) if s == "" { return "" } return s default: if n, ok := t.(jsonNumberStringer); ok { return strings.TrimSpace(n.String()) } s := strings.TrimSpace(fmt.Sprint(t)) if s == "" || s == "" { return "" } return s } }