298 lines
6.8 KiB
Go
298 lines
6.8 KiB
Go
|
package strcase
|
||
|
|
||
|
import "strings"
|
||
|
|
||
|
// WordCase is an enumeration of the ways to format a word.
|
||
|
type WordCase int
|
||
|
|
||
|
const (
|
||
|
// Original - Preserve the original input strcase
|
||
|
Original WordCase = iota
|
||
|
// LowerCase - All letters lower cased (example)
|
||
|
LowerCase
|
||
|
// UpperCase - All letters upper cased (EXAMPLE)
|
||
|
UpperCase
|
||
|
// TitleCase - Only first letter upper cased (Example)
|
||
|
TitleCase
|
||
|
// CamelCase - TitleCase except lower case first word (exampleText)
|
||
|
// Notably, even if the first word is an initialism, it will be lower
|
||
|
// cased. This is important for code generators where capital letters
|
||
|
// mean exported functions. i.e. jsonString(), not JSONString()
|
||
|
CamelCase
|
||
|
)
|
||
|
|
||
|
// We have 3 convert functions for performance reasons
|
||
|
// The general convert could handle everything, but is not optimized
|
||
|
//
|
||
|
// The other two functions are optimized for the general use cases - that is the non-custom caser functions
|
||
|
// Case 1: Any Case and supports Go Initialisms
|
||
|
// Case 2: UpperCase words, which don't need to support initialisms since everything is in upper case
|
||
|
|
||
|
// convertWithoutInitialims only works for to UpperCase and LowerCase
|
||
|
//nolint:gocyclo
|
||
|
func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) string {
|
||
|
input = strings.TrimSpace(input)
|
||
|
runes := []rune(input)
|
||
|
if len(runes) == 0 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
var b strings.Builder
|
||
|
b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
|
||
|
|
||
|
var prev, curr rune
|
||
|
next := runes[0] // 0 length will have already returned so safe to index
|
||
|
inWord := false
|
||
|
firstWord := true
|
||
|
for i := 0; i < len(runes); i++ {
|
||
|
prev = curr
|
||
|
curr = next
|
||
|
if i+1 == len(runes) {
|
||
|
next = 0
|
||
|
} else {
|
||
|
next = runes[i+1]
|
||
|
}
|
||
|
|
||
|
switch defaultSplitFn(prev, curr, next) {
|
||
|
case SkipSplit:
|
||
|
if inWord && delimiter != 0 {
|
||
|
b.WriteRune(delimiter)
|
||
|
}
|
||
|
inWord = false
|
||
|
continue
|
||
|
case Split:
|
||
|
if inWord && delimiter != 0 {
|
||
|
b.WriteRune(delimiter)
|
||
|
}
|
||
|
inWord = false
|
||
|
}
|
||
|
switch wordCase {
|
||
|
case UpperCase:
|
||
|
b.WriteRune(toUpper(curr))
|
||
|
case LowerCase:
|
||
|
b.WriteRune(toLower(curr))
|
||
|
case TitleCase:
|
||
|
if inWord {
|
||
|
b.WriteRune(toLower(curr))
|
||
|
} else {
|
||
|
b.WriteRune(toUpper(curr))
|
||
|
}
|
||
|
case CamelCase:
|
||
|
if inWord {
|
||
|
b.WriteRune(toLower(curr))
|
||
|
} else if firstWord {
|
||
|
b.WriteRune(toLower(curr))
|
||
|
firstWord = false
|
||
|
} else {
|
||
|
b.WriteRune(toUpper(curr))
|
||
|
}
|
||
|
default:
|
||
|
// Must be original case
|
||
|
b.WriteRune(curr)
|
||
|
}
|
||
|
inWord = inWord || true
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
// convertWithGoInitialisms changes a input string to a certain case with a
|
||
|
// delimiter, respecting go initialisms but not skip runes
|
||
|
//nolint:gocyclo
|
||
|
func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) string {
|
||
|
input = strings.TrimSpace(input)
|
||
|
runes := []rune(input)
|
||
|
if len(runes) == 0 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
var b strings.Builder
|
||
|
b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
|
||
|
|
||
|
firstWord := true
|
||
|
|
||
|
addWord := func(start, end int) {
|
||
|
if start == end {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if !firstWord && delimiter != 0 {
|
||
|
b.WriteRune(delimiter)
|
||
|
}
|
||
|
|
||
|
// Don't bother with initialisms if the word is longer than 5
|
||
|
// A quick proxy to avoid the extra memory allocations
|
||
|
if end-start <= 5 {
|
||
|
key := strings.ToUpper(string(runes[start:end]))
|
||
|
if golintInitialisms[key] {
|
||
|
if !firstWord || wordCase != CamelCase {
|
||
|
b.WriteString(key)
|
||
|
firstWord = false
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i := start; i < end; i++ {
|
||
|
r := runes[i]
|
||
|
switch wordCase {
|
||
|
case UpperCase:
|
||
|
panic("use convertWithoutInitialisms instead")
|
||
|
case LowerCase:
|
||
|
b.WriteRune(toLower(r))
|
||
|
case TitleCase:
|
||
|
if i == start {
|
||
|
b.WriteRune(toUpper(r))
|
||
|
} else {
|
||
|
b.WriteRune(toLower(r))
|
||
|
}
|
||
|
case CamelCase:
|
||
|
if !firstWord && i == start {
|
||
|
b.WriteRune(toUpper(r))
|
||
|
} else {
|
||
|
b.WriteRune(toLower(r))
|
||
|
}
|
||
|
default:
|
||
|
b.WriteRune(r)
|
||
|
}
|
||
|
}
|
||
|
firstWord = false
|
||
|
}
|
||
|
|
||
|
var prev, curr rune
|
||
|
next := runes[0] // 0 length will have already returned so safe to index
|
||
|
wordStart := 0
|
||
|
for i := 0; i < len(runes); i++ {
|
||
|
prev = curr
|
||
|
curr = next
|
||
|
if i+1 == len(runes) {
|
||
|
next = 0
|
||
|
} else {
|
||
|
next = runes[i+1]
|
||
|
}
|
||
|
|
||
|
switch defaultSplitFn(prev, curr, next) {
|
||
|
case Split:
|
||
|
addWord(wordStart, i)
|
||
|
wordStart = i
|
||
|
case SkipSplit:
|
||
|
addWord(wordStart, i)
|
||
|
wordStart = i + 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wordStart != len(runes) {
|
||
|
addWord(wordStart, len(runes))
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
// convert changes a input string to a certain case with a delimiter,
|
||
|
// respecting arbitrary initialisms and skip characters
|
||
|
//nolint:gocyclo
|
||
|
func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase,
|
||
|
initialisms map[string]bool) string {
|
||
|
input = strings.TrimSpace(input)
|
||
|
runes := []rune(input)
|
||
|
if len(runes) == 0 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
var b strings.Builder
|
||
|
b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
|
||
|
|
||
|
firstWord := true
|
||
|
var skipIndexes []int
|
||
|
|
||
|
addWord := func(start, end int) {
|
||
|
// If you have nothing good to say, say nothing at all
|
||
|
if start == end || len(skipIndexes) == end-start {
|
||
|
skipIndexes = nil
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// If you have something to say, start with a delimiter
|
||
|
if !firstWord && delimiter != 0 {
|
||
|
b.WriteRune(delimiter)
|
||
|
}
|
||
|
|
||
|
// Check if you're an initialism
|
||
|
// Note - we don't check skip characters here since initialisms
|
||
|
// will probably never have junk characters in between
|
||
|
// I'm open to it if there is a use case
|
||
|
if initialisms != nil {
|
||
|
var word strings.Builder
|
||
|
for i := start; i < end; i++ {
|
||
|
word.WriteRune(toUpper(runes[i]))
|
||
|
}
|
||
|
key := word.String()
|
||
|
if initialisms[key] {
|
||
|
if !firstWord || wordCase != CamelCase {
|
||
|
b.WriteString(key)
|
||
|
firstWord = false
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
skipIdx := 0
|
||
|
for i := start; i < end; i++ {
|
||
|
if len(skipIndexes) > 0 && skipIdx < len(skipIndexes) && i == skipIndexes[skipIdx] {
|
||
|
skipIdx++
|
||
|
continue
|
||
|
}
|
||
|
r := runes[i]
|
||
|
switch wordCase {
|
||
|
case UpperCase:
|
||
|
b.WriteRune(toUpper(r))
|
||
|
case LowerCase:
|
||
|
b.WriteRune(toLower(r))
|
||
|
case TitleCase:
|
||
|
if i == start {
|
||
|
b.WriteRune(toUpper(r))
|
||
|
} else {
|
||
|
b.WriteRune(toLower(r))
|
||
|
}
|
||
|
case CamelCase:
|
||
|
if !firstWord && i == start {
|
||
|
b.WriteRune(toUpper(r))
|
||
|
} else {
|
||
|
b.WriteRune(toLower(r))
|
||
|
}
|
||
|
default:
|
||
|
b.WriteRune(r)
|
||
|
}
|
||
|
}
|
||
|
firstWord = false
|
||
|
skipIndexes = nil
|
||
|
}
|
||
|
|
||
|
var prev, curr rune
|
||
|
next := runes[0] // 0 length will have already returned so safe to index
|
||
|
wordStart := 0
|
||
|
for i := 0; i < len(runes); i++ {
|
||
|
prev = curr
|
||
|
curr = next
|
||
|
if i+1 == len(runes) {
|
||
|
next = 0
|
||
|
} else {
|
||
|
next = runes[i+1]
|
||
|
}
|
||
|
|
||
|
switch fn(prev, curr, next) {
|
||
|
case Skip:
|
||
|
skipIndexes = append(skipIndexes, i)
|
||
|
case Split:
|
||
|
addWord(wordStart, i)
|
||
|
wordStart = i
|
||
|
case SkipSplit:
|
||
|
addWord(wordStart, i)
|
||
|
wordStart = i + 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wordStart != len(runes) {
|
||
|
addWord(wordStart, len(runes))
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|