-
-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #368 from danielgtaylor/vendor-casing
fix: vendor casing library
- Loading branch information
Showing
7 changed files
with
420 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
// Package casing helps convert between CamelCase, snake_case, and others. It | ||
// includes an intelligent `Split` function to take almost any input and then | ||
// convert it to any type of casing. | ||
package casing | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
"unicode" | ||
) | ||
|
||
// Borrowed from Go lint (https://github.com/golang/lint) | ||
var commonInitialisms = 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, | ||
|
||
// Media initialisms | ||
"1080P": true, | ||
"2D": true, | ||
"3D": true, | ||
"4K": true, | ||
"8K": true, | ||
"AAC": true, | ||
"AC3": true, | ||
"CDN": true, | ||
"DASH": true, | ||
"DRM": true, | ||
"DVR": true, | ||
"EAC3": true, | ||
"FPS": true, | ||
"GOP": true, | ||
"H264": true, | ||
"H265": true, | ||
"HD": true, | ||
"HLS": true, | ||
"MJPEG": true, | ||
"MP2T": true, | ||
"MP3": true, | ||
"MP4": true, | ||
"MPEG2": true, | ||
"MPEG4": true, | ||
"NTSC": true, | ||
"PCM": true, | ||
"RGB": true, | ||
"RGBA": true, | ||
"RTMP": true, | ||
"RTP": true, | ||
"SCTE": true, | ||
"SCTE35": true, | ||
"SMPTE": true, | ||
"UPID": true, | ||
"UPIDS": true, | ||
"VOD": true, | ||
"YUV420": true, | ||
"YUV422": true, | ||
"YUV444": true, | ||
} | ||
|
||
var commonSuffixes = map[string]bool{ | ||
// E.g. 2D, 3D | ||
"D": true, | ||
// E.g. 100GB | ||
"GB": true, | ||
// E.g. 4K, 8K | ||
"K": true, | ||
// E.g. 100KB | ||
"KB": true, | ||
// E.g. 64kbps | ||
"KBPS": true, | ||
// E.g. 100MB | ||
"MB": true, | ||
// E.g. 2500mbps | ||
"MPBS": true, | ||
// E.g. 1080P | ||
"P": true, | ||
// E.g. 100TB | ||
"TB": true, | ||
} | ||
|
||
// TransformFunc is used to transform parts of a split string during joining. | ||
type TransformFunc func(string) string | ||
|
||
// Identity is the identity transformation function. | ||
func Identity(part string) string { | ||
return part | ||
} | ||
|
||
// Initialism converts common initialisms like ID and HTTP to uppercase. | ||
func Initialism(part string) string { | ||
if u := strings.ToUpper(part); commonInitialisms[u] { | ||
return u | ||
} | ||
return part | ||
} | ||
|
||
// State machine for splitting. | ||
const ( | ||
stateNone = 0 | ||
stateLower = 1 | ||
stateFirstUpper = 2 | ||
stateUpper = 3 | ||
stateSymbol = 4 | ||
) | ||
|
||
// Split a value intelligently, taking into account different casing styles, | ||
// numbers, symbols, etc. | ||
// | ||
// // Returns: ["HTTP", "Server", "2020"] | ||
// casing.Split("HTTPServer_2020") | ||
func Split(value string) []string { | ||
results := []string{} | ||
start := 0 | ||
state := stateNone | ||
|
||
for i, c := range value { | ||
// Regardless of state, these always break words. Handles kabob and snake | ||
// casing, respectively. | ||
if unicode.IsSpace(c) || unicode.IsPunct(c) { | ||
if i-start > 0 { | ||
results = append(results, value[start:i]) | ||
} | ||
start = i + 1 | ||
state = stateNone | ||
continue | ||
} | ||
|
||
switch { | ||
case state != stateFirstUpper && state != stateUpper && unicode.IsUpper(c): | ||
// Initial uppercase, might start a word, e.g. Camel | ||
if start != i { | ||
results = append(results, value[start:i]) | ||
start = i | ||
} | ||
state = stateFirstUpper | ||
case state == stateFirstUpper && unicode.IsUpper(c): | ||
// String of uppercase to be grouped, e.g. HTTP | ||
state = stateUpper | ||
case state != stateSymbol && !unicode.IsLetter(c): | ||
// Anything -> non-letter | ||
if start != i { | ||
results = append(results, value[start:i]) | ||
start = i | ||
} | ||
state = stateSymbol | ||
case state != stateLower && unicode.IsLower(c): | ||
if state == stateUpper { | ||
// Multi-character uppercase to lowercase. Last item in the uppercase | ||
// is part of the lowercase string, e.g. HTTPServer. | ||
if i > 0 && start != i-1 { | ||
results = append(results, value[start:i-1]) | ||
start = i - 1 | ||
} | ||
} else if state != stateFirstUpper { | ||
// End of a non-uppercase or non-lowercase string. Ignore the first | ||
// upper state as it's part of the same word. | ||
if i > 0 && start != i { | ||
results = append(results, value[start:i]) | ||
start = i | ||
} | ||
} | ||
state = stateLower | ||
} | ||
} | ||
|
||
// Include whatever is at the end of the string. | ||
if start < len(value) { | ||
results = append(results, value[start:]) | ||
} | ||
|
||
return results | ||
} | ||
|
||
// Join will combine split parts back together with the given separator and | ||
// optional transform functions. | ||
func Join(parts []string, sep string, transform ...TransformFunc) string { | ||
for i := 0; i < len(parts); i++ { | ||
for _, t := range transform { | ||
parts[i] = t(parts[i]) | ||
|
||
if parts[i] == "" { | ||
// Transformer completely removed this part. | ||
parts = append(parts[:i], parts[i+1:]...) | ||
i-- | ||
} | ||
} | ||
} | ||
|
||
return strings.Join(parts, sep) | ||
} | ||
|
||
// MergeNumbers will merge some number parts with their adjacent letter parts | ||
// to support a smarter delimited join. For example, `h264` instead of `h_264` | ||
// or `mp3-player` instead of `mp-3-player`. You can pass suffixes for right | ||
// aligning certain items, e.g. `K` to get `MODE_4K` instead of `MODE4_K`. If | ||
// no suffixes are passed, then a default set of common ones is used. Pass an | ||
// empty string to disable the default. | ||
func MergeNumbers(parts []string, suffixes ...string) []string { | ||
// TODO: should we do this in-place instead? | ||
results := make([]string, 0, len(parts)) | ||
prevNum := false | ||
|
||
suffixLookup := map[string]bool{} | ||
for _, word := range suffixes { | ||
suffixLookup[strings.ToUpper(word)] = true | ||
} | ||
if len(suffixes) == 0 { | ||
suffixLookup = commonSuffixes | ||
} | ||
|
||
for i := 0; i < len(parts); i++ { | ||
part := parts[i] | ||
if _, err := strconv.Atoi(part); err == nil { | ||
// This part is a number! | ||
|
||
// Special case: right aligned word | ||
if i < len(parts)-1 && suffixLookup[strings.ToUpper(parts[i+1])] { | ||
results = append(results, part+parts[i+1]) | ||
i++ | ||
continue | ||
} | ||
|
||
if !prevNum { | ||
if i == 0 { | ||
// First item must always append. | ||
results = append(results, part) | ||
} else { | ||
// Concatenate the number to the previous non-number piece. | ||
results[len(results)-1] += part | ||
} | ||
prevNum = true | ||
continue | ||
} | ||
|
||
prevNum = true | ||
} else { | ||
// Special case: first part is a number, second part is not. | ||
if i == 1 && prevNum { | ||
results[0] += part | ||
prevNum = false | ||
continue | ||
} | ||
|
||
prevNum = false | ||
} | ||
|
||
results = append(results, part) | ||
} | ||
|
||
return results | ||
} | ||
|
||
// Camel returns a CamelCase version of the input. If no transformation | ||
// functions are passed in, then strings.ToLower is used. This can be disabled | ||
// by passing in the identity function. | ||
func Camel(value string, transform ...TransformFunc) string { | ||
if transform == nil { | ||
transform = []TransformFunc{strings.ToLower} | ||
} | ||
t := append(transform, strings.Title) //nolint:staticcheck | ||
return Join(Split(value), "", t...) | ||
} | ||
|
||
// LowerCamel returns a lowerCamelCase version of the input. | ||
func LowerCamel(value string, transform ...TransformFunc) string { | ||
runes := []rune(Camel(value, transform...)) | ||
runes[0] = unicode.ToLower(runes[0]) | ||
return string(runes) | ||
} | ||
|
||
// Snake returns a snake_case version of the input. | ||
func Snake(value string, transform ...TransformFunc) string { | ||
if transform == nil { | ||
transform = []TransformFunc{strings.ToLower} | ||
} | ||
return Join(MergeNumbers(Split(value)), "_", transform...) | ||
} | ||
|
||
// Kebab returns a kabob-case version of the input. | ||
func Kebab(value string, transform ...TransformFunc) string { | ||
if transform == nil { | ||
transform = []TransformFunc{strings.ToLower} | ||
} | ||
return Join(MergeNumbers(Split(value)), "-", transform...) | ||
} |
Oops, something went wrong.