package utils // Genre display rendering (faceted plan Batch B / Phase 7): the // canonically-lowercase genre value is title-cased at DISPLAY time // only — never in the DB. Canonical home: the filter panel (services), // templ (templates wraps this), and the TS port in bookshelf.ts all // share these exact rules. import ( "strings" "unicode" ) // genreSmallWords stay lowercase in title-case rendering (except the // first word of the value). var genreSmallWords = map[string]bool{ "a": true, "an": true, "and": true, "of": true, "the": true, "in": true, "on": true, "for": true, "to": true, "or": true, } // PrettifyGenre renders the canonically-lowercase genre value in title // case at display time (Batch B transform — never in the DB). Only // values that are ENTIRELY lowercase get transformed; anything else is // returned verbatim, protecting user-entered mixed case. Small words // stay down except in the first position; "&" has no case and passes // through. "science fiction" → "Science Fiction", "sword & sorcery" → // "Sword & Sorcery". func PrettifyGenre(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "" } if trimmed != strings.ToLower(trimmed) { return trimmed // mixed case = user-authored; display as written } words := strings.Fields(trimmed) out := make([]string, len(words)) for i, w := range words { if i > 0 && genreSmallWords[w] { out[i] = w continue } runes := []rune(w) out[i] = string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)) } return strings.Join(out, " ") }