2026-08-16 16:57:36 +02:00
|
|
|
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
|
2026-08-16 23:42:00 +02:00
|
|
|
// stripped via v1PlainDescription — use for meta/SEO and feed normalize only.
|
|
|
|
|
// V1 process poll / formula bodies use DescriptionFromAny (keeps markup).
|
2026-08-16 16:57:36 +02:00
|
|
|
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 == "<nil>" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return v1PlainDescription(s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 23:42:00 +02:00
|
|
|
// 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 == "<nil>" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return v1PreserveDescription(s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 16:57:36 +02:00
|
|
|
// 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 == "<nil>" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
default:
|
|
|
|
|
if n, ok := t.(jsonNumberStringer); ok {
|
|
|
|
|
return strings.TrimSpace(n.String())
|
|
|
|
|
}
|
|
|
|
|
s := strings.TrimSpace(fmt.Sprint(t))
|
|
|
|
|
if s == "" || s == "<nil>" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|