Repository: go-sprout/sprout
Pull request #35 (2024-05-16 00:10:38)

fix: camelcase and pascalcase transformation logic
## Description
In sprig, `camelCase` return a `PascalCase` and don't respect the separation logic between both functions.

## Changes
- Introduce a `toPascalCase` function to transform a string to a `PascalCase`
- Fix the behivour of `toCamelCase` function to transform a string to a `camelCase`

## Fixes https://github.com/Masterminds/sprig/issues/328

## Checklist
- [ ] I have read the **CONTRIBUTING.md** document.
- [ ] My code follows the code style of this project.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] I have updated the documentation accordingly.
- [ ] This change requires a change to the documentation on the website.

## Additional Information

This edit are related to https://github.com/Masterminds/sprig/issues/328

Backport ticket from @ghostsquad


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
All modified and coverable lines are covered by tests :white_check_mark:


| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [strings\_functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/35?src=pr&el=tree&filepath=strings_functions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-c3RyaW5nc19mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
All modified and coverable lines are covered by tests :white_check_mark:


| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [strings\_functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/35?src=pr&el=tree&filepath=strings_functions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-c3RyaW5nc19mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
All modified and coverable lines are covered by tests :white_check_mark:


| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/35?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [strings\_functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/35?src=pr&el=tree&filepath=strings_functions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-c3RyaW5nc19mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |



Patch:
diff --git a/strings_functions.go b/strings_functions.go
index 4f66ae0..fa793b1 100644
--- a/strings_functions.go
+++ b/strings_functions.go
@@ -43,16 +43,17 @@ import (
 //		}
 //	 Use `style` to transform "ExampleText" to "example_text"
 type caseStyle struct {
-	Separator      rune // Character that separates words.
-	CapitalizeNext bool // Whether to capitalize the first character of each word.
-	ForceLowercase bool // Whether to force all characters to lowercase.
-	ForceUppercase bool // Whether to force all characters to uppercase.
+	Separator       rune // Character that separates words.
+	CapitalizeFirst bool // Whether to capitalize the first character of the string.
+	CapitalizeNext  bool // Whether to capitalize the first character of each word.
+	ForceLowercase  bool // Whether to force all characters to lowercase.
+	ForceUppercase  bool // Whether to force all characters to uppercase.
 }
 
 var (
-	camelCaseStyle    = caseStyle{Separator: -1, CapitalizeNext: true, ForceLowercase: true}
+	camelCaseStyle    = caseStyle{Separator: -1, CapitalizeNext: true, CapitalizeFirst: false, ForceLowercase: true}
 	kebabCaseStyle    = caseStyle{Separator: '-', ForceLowercase: true}
-	pascalCaseStyle   = caseStyle{Separator: -1, CapitalizeNext: true}
+	pascalCaseStyle   = caseStyle{Separator: -1, CapitalizeFirst: true, CapitalizeNext: true}
 	snakeCaseStyle    = caseStyle{Separator: '_', ForceLowercase: true}
 	dotCaseStyle      = caseStyle{Separator: '.', ForceLowercase: true}
 	pathCaseStyle     = caseStyle{Separator: '/', ForceLowercase: true}
@@ -702,8 +703,13 @@ func (fh *FunctionHandler) Squote(elements ...any) string {
 func (fh *FunctionHandler) transformString(style caseStyle, str string) string {
 	var result strings.Builder
 	result.Grow(len(str) + 10) // Allocate a bit more for potential separators
-	capitalizeNext := style.CapitalizeNext
-	var lastRune, nextRune rune = 0, 0
+
+	var capitalizeNext = style.CapitalizeNext
+	var lastRune, lastLetter, nextRune rune = 0, 0, 0
+
+	if !style.CapitalizeFirst {
+		capitalizeNext = false
+	}
 
 	for i, r := range str {
 		if i+1 < len(str) {
@@ -714,7 +720,10 @@ func (fh *FunctionHandler) transformString(style caseStyle, str string) string {
 			if style.Separator != -1 && (lastRune != style.Separator) {
 				result.WriteRune(style.Separator)
 			}
-			capitalizeNext = true
+			if lastLetter != 0 {
+				capitalizeNext = true
+			}
+
 			lastRune = style.Separator
 			continue
 		}
@@ -739,7 +748,11 @@ func (fh *FunctionHandler) transformString(style caseStyle, str string) string {
 		} else {
 			result.WriteRune(r)
 		}
+
 		lastRune = r // Update lastRune to the current rune
+		if unicode.IsLetter(r) {
+			lastLetter = r
+		}
 	}
 
 	return result.String()


Test patch:
diff --git a/strings_functions_test.go b/strings_functions_test.go
index a46f2f3..684cc3f 100644
--- a/strings_functions_test.go
+++ b/strings_functions_test.go
@@ -279,16 +279,17 @@ func TestSquote(t *testing.T) {
 func TestToCamelCase(t *testing.T) {
 	var tests = testCases{
 		{"TestEmpty", `{{ "" | toCamelCase }}`, "", nil},
-		{"TestCamelCase", `{{ "foo bar" | toCamelCase }}`, "FooBar", nil},
-		{"TestCamelCaseWithSpace", `{{ "foo  bar" | toCamelCase }}`, "FooBar", nil},
-		{"TestCamelCaseWithUnderscore", `{{ "foo_bar" | toCamelCase }}`, "FooBar", nil},
-		{"TestCamelCaseWithHyphen", `{{ "foo-bar" | toCamelCase }}`, "FooBar", nil},
-		{"TestCamelCaseWithMixed", `{{ "foo-bar_baz" | toCamelCase }}`, "FooBarBaz", nil},
-		{"", `{{ toCamelCase "_complex__case_" }}`, "ComplexCase", nil},
-		{"", `{{ toCamelCase "_camel_case" }}`, "CamelCase", nil},
-		{"", `{{ toCamelCase "http_server" }}`, "HttpServer", nil},
-		{"", `{{ toCamelCase "no_https" }}`, "NoHttps", nil},
-		{"", `{{ toCamelCase "all" }}`, "All", nil},
+		{"TestCamelCase", `{{ "foo bar" | toCamelCase }}`, "fooBar", nil},
+		{"TestCamelCaseWithUpperCase", `{{ "FoO  bar" | toCamelCase }}`, "fooBar", nil},
+		{"TestCamelCaseWithSpace", `{{ "foo  bar" | toCamelCase }}`, "fooBar", nil},
+		{"TestCamelCaseWithUnderscore", `{{ "foo_bar" | toCamelCase }}`, "fooBar", nil},
+		{"TestCamelCaseWithHyphen", `{{ "foo-bar" | toCamelCase }}`, "fooBar", nil},
+		{"TestCamelCaseWithMixed", `{{ "foo-bar_baz" | toCamelCase }}`, "fooBarBaz", nil},
+		{"", `{{ toCamelCase "___complex__case_" }}`, "complexCase", nil},
+		{"", `{{ toCamelCase "_camel_case" }}`, "camelCase", nil},
+		{"", `{{ toCamelCase "http_server" }}`, "httpServer", nil},
+		{"", `{{ toCamelCase "no_https" }}`, "noHttps", nil},
+		{"", `{{ toCamelCase "all" }}`, "all", nil},
 	}
 
 	runTestCases(t, tests)