// Package kace provides common case conversion functions which take into // consideration common initialisms. package kace import ( "fmt" "strings" "unicode" "github.com/codemodus/kace/ktrie" ) const ( kebabDelim = '-' snakeDelim = '_' none = rune(-1) ) var ( ciTrie *ktrie.KTrie ) func init() { var err error if ciTrie, err = ktrie.NewKTrie(ciMap); err != nil { panic(err) } } // Camel returns a camelCased string. func Camel(s string) string { return camelCase(ciTrie, s, false) } // Pascal returns a PascalCased string. func Pascal(s string) string { return camelCase(ciTrie, s, true) } // Kebab returns a kebab-cased string with all lowercase letters. func Kebab(s string) string { return delimitedCase(s, kebabDelim, false) } // KebabUpper returns a KEBAB-CASED string with all upper case letters. func KebabUpper(s string) string { return delimitedCase(s, kebabDelim, true) } // Snake returns a snake_cased string with all lowercase letters. func Snake(s string) string { return delimitedCase(s, snakeDelim, false) } // SnakeUpper returns a SNAKE_CASED string with all upper case letters. func SnakeUpper(s string) string { return delimitedCase(s, snakeDelim, true) } // Kace provides common case conversion methods which take into // consideration common initialisms set by the user. type Kace struct { t *ktrie.KTrie } // New returns a pointer to an instance of kace loaded with a common // initialsms trie based on the provided map. Before conversion to a // trie, the provided map keys are all upper cased. func New(initialisms map[string]bool) (*Kace, error) { ci := initialisms if ci == nil { ci = map[string]bool{} } ci = sanitizeCI(ci) t, err := ktrie.NewKTrie(ci) if err != nil { return nil, fmt.Errorf("kace: cannot create trie: %s", err) } k := &Kace{ t: t, } return k, nil } // Camel returns a camelCased string. func (k *Kace) Camel(s string) string { return camelCase(k.t, s, false) } // Pascal returns a PascalCased string. func (k *Kace) Pascal(s string) string { return camelCase(k.t, s, true) } // Snake returns a snake_cased string with all lowercase letters. func (k *Kace) Snake(s string) string { return delimitedCase(s, snakeDelim, false) } // SnakeUpper returns a SNAKE_CASED string with all upper case letters. func (k *Kace) SnakeUpper(s string) string { return delimitedCase(s, snakeDelim, true) } // Kebab returns a kebab-cased string with all lowercase letters. func (k *Kace) Kebab(s string) string { return delimitedCase(s, kebabDelim, false) } // KebabUpper returns a KEBAB-CASED string with all upper case letters. func (k *Kace) KebabUpper(s string) string { return delimitedCase(s, kebabDelim, true) } func camelCase(t *ktrie.KTrie, s string, ucFirst bool) string { rs := []rune(s) offset := 0 prev := none for i := 0; i < len(rs); i++ { r := rs[i] switch { case unicode.IsLetter(r): ucCurr := isToBeUpper(r, prev, ucFirst) if ucCurr || isSegmentStart(r, prev) { prv, skip := updateRunes(rs, i, offset, t, ucCurr) if skip > 0 { i += skip - 1 prev = prv continue } } prev = updateRune(rs, i, offset, ucCurr) continue case unicode.IsNumber(r): prev = updateRune(rs, i, offset, false) continue default: prev = r offset-- } } return string(rs[:len(rs)+offset]) } func isToBeUpper(curr, prev rune, ucFirst bool) bool { if prev == none { return ucFirst } return isSegmentStart(curr, prev) } func isSegmentStart(curr, prev rune) bool { if !unicode.IsLetter(prev) || unicode.IsUpper(curr) && unicode.IsLower(prev) { return true } return false } func updateRune(rs []rune, i, offset int, upper bool) rune { r := rs[i] dest := i + offset if dest < 0 || i > len(rs)-1 { panic("this function has been used or designed incorrectly") } fn := unicode.ToLower if upper { fn = unicode.ToUpper } rs[dest] = fn(r) return r } func updateRunes(rs []rune, i, offset int, t *ktrie.KTrie, upper bool) (rune, int) { r := rs[i] ns := nextSegment(rs, i) ct := len(ns) if ct < t.MinDepth() || ct > t.MaxDepth() || !t.FindAsUpper(ns) { return r, 0 } for j := i; j < i+ct; j++ { r = updateRune(rs, j, offset, upper) } return r, ct } func nextSegment(rs []rune, i int) []rune { for j := i; j < len(rs); j++ { if !unicode.IsLetter(rs[j]) && !unicode.IsNumber(rs[j]) { return rs[i:j] } if j == len(rs)-1 { return rs[i : j+1] } } return nil } func delimitedCase(s string, delim rune, upper bool) string { buf := make([]rune, 0, len(s)*2) for i := len(s); i > 0; i-- { switch { case unicode.IsLetter(rune(s[i-1])): if i < len(s) && unicode.IsUpper(rune(s[i])) { if i > 1 && unicode.IsLower(rune(s[i-1])) || i < len(s)-2 && unicode.IsLower(rune(s[i+1])) { buf = append(buf, delim) } } buf = appendCased(buf, upper, rune(s[i-1])) case unicode.IsNumber(rune(s[i-1])): if i == len(s) || i == 1 || unicode.IsNumber(rune(s[i])) { buf = append(buf, rune(s[i-1])) continue } buf = append(buf, delim, rune(s[i-1])) default: if i == len(s) { continue } buf = append(buf, delim) } } reverse(buf) return string(buf) } func appendCased(rs []rune, upper bool, r rune) []rune { if upper { rs = append(rs, unicode.ToUpper(r)) return rs } rs = append(rs, unicode.ToLower(r)) return rs } func reverse(s []rune) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } var ( // github.com/golang/lint/blob/master/lint.go ciMap = map[string]bool{ "ACL": true, "API": true, "ASCII": true, "CPU": true, "CSS": true, "DNS": true, "EOF": true, "GUID": true, "HTML": true, "HTTP": true, "HTTPS": true, "ID": true, "IP": true, "JSON": true, "LHS": true, "QPS": true, "RAM": true, "RHS": true, "RPC": true, "SLA": true, "SMTP": true, "SQL": true, "SSH": true, "TCP": true, "TLS": true, "TTL": true, "UDP": true, "UI": true, "UID": true, "UUID": true, "URI": true, "URL": true, "UTF8": true, "VM": true, "XML": true, "XMPP": true, "XSRF": true, "XSS": true, } ) func sanitizeCI(m map[string]bool) map[string]bool { r := map[string]bool{} for k := range m { fn := func(r rune) rune { if !unicode.IsLetter(r) && !unicode.IsNumber(r) { return -1 } return r } k = strings.Map(fn, k) k = strings.ToUpper(k) if k == "" { continue } r[k] = true } return r }