Repository: go-sprout/sprout
Pull request #62 (2024-08-22 15:23:31)

feat: add strings capitalization functions
## Description
When we manipulate strings in template, we need sometimes to capitalize or uncapitalize a string. A good example are when we manipulate go file and want to turn public/private functions.

## Changes
- Add `capitalize` functions on strings registry
- Add `uncapitalize` functions on strings registry

## Checklist
- [x] I have read the **CONTRIBUTING.md** document.
- [x] My code follows the code style of this project.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] I have updated the documentation accordingly.
- [x] This change requires a change to the documentation on the website.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
Attention: Patch coverage is `90.00000%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Patch % | Lines |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | 88.8% | [2 Missing :warning: ](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) |

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `99.0% <88.8%> (-1.0%)` | :arrow_down: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
Attention: Patch coverage is `90.00000%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Patch % | Lines |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | 88.8% | [2 Missing :warning: ](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) |

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `99.0% <88.8%> (-1.0%)` | :arrow_down: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
Attention: Patch coverage is `90.00000%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Patch % | Lines |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | 88.8% | [2 Missing :warning: ](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) |

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `99.0% <88.8%> (-1.0%)` | :arrow_down: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
Attention: Patch coverage is `87.50000%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Patch % | Lines |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | 85.7% | [2 Missing :warning: ](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) |

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `98.9% <85.7%> (-1.1%)` | :arrow_down: |

... and [22 files with indirect coverage changes](https://app.codecov.io/gh/go-sprout/sprout/pull/62/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) Report
Attention: Patch coverage is `90.00000%` with `2 lines` in your changes missing coverage. Please review.

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Patch % | Lines |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | 88.8% | [2 Missing :warning: ](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) |

| [Files](https://app.codecov.io/gh/go-sprout/sprout/pull/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `99.0% <88.8%> (-1.0%)` | :arrow_down: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?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/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |

... and [22 files with indirect coverage changes](https://app.codecov.io/gh/go-sprout/sprout/pull/62/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?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/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?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/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-sprout/sprout/pull/62?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/62?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout) | Coverage Δ | |
|---|---|---|
| [registry/strings/functions.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Ffunctions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9mdW5jdGlvbnMuZ28=) | `100.0% <100.0%> (ø)` | |
| [registry/strings/strings.go](https://app.codecov.io/gh/go-sprout/sprout/pull/62?src=pr&el=tree&filepath=registry%2Fstrings%2Fstrings.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-sprout#diff-cmVnaXN0cnkvc3RyaW5ncy9zdHJpbmdzLmdv) | `96.2% <100.0%> (+0.1%)` | :arrow_up: |


Patch:
diff --git a/docs/registries/reflect.md b/docs/registries/reflect.md
index eaef8fb..807ae5e 100644
--- a/docs/registries/reflect.md
+++ b/docs/registries/reflect.md
@@ -90,6 +90,24 @@ The function returns the kind (category) of the provided value (`src`) as a stri
 {% endtab %}
 {% endtabs %}
 
+### <mark style="color:purple;">hasField</mark>
+
+The function checks the presence of a field with the specified name (`name`) in the provided struct (`src`). It returns `true` if the field exists.
+
+<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">HasField(name string, src any) bool
+</code></pre></td></tr><tr><td>Must version</td><td><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr></tbody></table>
+
+{% tabs %}
+{% tab title="Template Example" %}
+```go
+{{ hasField "someExistingField" .someStruct }} // Output: true
+{{ hasField "someNonExistingField" .someStruct }} // Output: false
+{{ .someStruct | hasField "someExistingField" }} // Output: true
+{{ .someStruct | hasField "someNonExistingField" }} // Output: false
+```
+{% endtab %}
+{% endtabs %}
+
 ### <mark style="color:purple;">deepEqual</mark>
 
 The function checks if two variables, `x` and `y`, are deeply equal by comparing their values and structures using `reflect.DeepEqual`.
diff --git a/docs/registries/strings.md b/docs/registries/strings.md
index 6aa46ec..80cda98 100644
--- a/docs/registries/strings.md
+++ b/docs/registries/strings.md
@@ -520,6 +520,40 @@ Switches the case of each letter in a string, converting lowercase to uppercase
 {% endtab %}
 {% endtabs %}
 
+### <mark style="color:purple;">capitalize</mark>
+
+Uppercases the first letter of a string while leaving the rest of the string unchanged.
+
+<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">capitalize(str string) string
+</code></pre></td></tr><tr><td>Must version</td><td><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr></tbody></table>
+
+{% tabs %}
+{% tab title="Template Example" %}
+```go
+{{ "hello world" | capitalize }} // Output: "Hello world"
+{{ "123boo_bar" | capitalize }} // Output: 123Boo_bar
+{{ " Fe bar" | capitalize }} // Output: " Fe bar"
+```
+{% endtab %}
+{% endtabs %}
+
+### <mark style="color:purple;">uncapitalize</mark>
+
+Lowercases the first letter of a string while leaving the rest of the string unchanged.
+
+<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">uncapitalize(str string) string
+</code></pre></td></tr><tr><td>Must version</td><td><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr></tbody></table>
+
+{% tabs %}
+{% tab title="Template Example" %}
+```go
+{{ "Hello World" | uncapitalize }} // Output: "hello World"
+{{ "123Boo_bar" | uncapitalize }} // Output: 123boo_bar
+{{ " Fe bar" | uncapitalize }} // Output: " fe bar"
+```
+{% endtab %}
+{% endtabs %}
+
 ### <mark style="color:purple;">split</mark>
 
 Divides a string into a map of parts based on a specified separator, returning a collection of the split components.
diff --git a/registry/reflect/functions.go b/registry/reflect/functions.go
index 08f2ecc..6e6c516 100644
--- a/registry/reflect/functions.go
+++ b/registry/reflect/functions.go
@@ -102,6 +102,29 @@ func (rr *ReflectRegistry) KindOf(src any) string {
 	return reflect.ValueOf(src).Kind().String()
 }
 
+// HasField checks whether a struct has a field with a given name.
+//
+// Parameters:
+//
+//	name string - the name of the field that is being checked.
+//	src any - the struct that is being checked.
+//
+// Returns:
+//
+//	bool - true if the struct 'src' contains a field with the name 'name', false otherwise.
+//
+// Example:
+//
+//	{{ hasField "someExistingField" .someStruct }} // Output: true
+//	{{ hasField "someNonExistingField" .someStruct }} // Output: false
+func (rr *ReflectRegistry) HasField(name string, src any) bool {
+	rv := reflect.Indirect(reflect.ValueOf(src))
+	if rv.Kind() != reflect.Struct {
+		return false
+	}
+	return rv.FieldByName(name).IsValid()
+}
+
 // DeepEqual determines if two variables, 'x' and 'y', are deeply equal.
 // It uses reflect.DeepEqual to evaluate equality.
 //
diff --git a/registry/reflect/reflect.go b/registry/reflect/reflect.go
index 98bab4d..8fb1f31 100644
--- a/registry/reflect/reflect.go
+++ b/registry/reflect/reflect.go
@@ -23,14 +23,15 @@ func (rr *ReflectRegistry) LinkHandler(fh sprout.Handler) error {
 }
 
 // RegisterFunctions registers all functions of the registry.
-func (nr *ReflectRegistry) RegisterFunctions(funcsMap sprout.FunctionMap) error {
-	sprout.AddFunction(funcsMap, "typeIs", nr.TypeIs)
-	sprout.AddFunction(funcsMap, "typeIsLike", nr.TypeIsLike)
-	sprout.AddFunction(funcsMap, "typeOf", nr.TypeOf)
-	sprout.AddFunction(funcsMap, "kindIs", nr.KindIs)
-	sprout.AddFunction(funcsMap, "kindOf", nr.KindOf)
-	sprout.AddFunction(funcsMap, "deepEqual", nr.DeepEqual)
-	sprout.AddFunction(funcsMap, "deepCopy", nr.DeepCopy)
-	sprout.AddFunction(funcsMap, "mustDeepCopy", nr.MustDeepCopy)
+func (rr *ReflectRegistry) RegisterFunctions(funcsMap sprout.FunctionMap) error {
+	sprout.AddFunction(funcsMap, "typeIs", rr.TypeIs)
+	sprout.AddFunction(funcsMap, "typeIsLike", rr.TypeIsLike)
+	sprout.AddFunction(funcsMap, "typeOf", rr.TypeOf)
+	sprout.AddFunction(funcsMap, "kindIs", rr.KindIs)
+	sprout.AddFunction(funcsMap, "kindOf", rr.KindOf)
+	sprout.AddFunction(funcsMap, "hasField", rr.HasField)
+	sprout.AddFunction(funcsMap, "deepEqual", rr.DeepEqual)
+	sprout.AddFunction(funcsMap, "deepCopy", rr.DeepCopy)
+	sprout.AddFunction(funcsMap, "mustDeepCopy", rr.MustDeepCopy)
 	return nil
 }
diff --git a/registry/strings/functions.go b/registry/strings/functions.go
index c5fac6c..f72619d 100644
--- a/registry/strings/functions.go
+++ b/registry/strings/functions.go
@@ -682,6 +682,52 @@ func (sr *StringsRegistry) SwapCase(str string) string {
 	}, str)
 }
 
+// Capitalize capitalizes the first letter of 'str'.
+//
+// Parameters:
+//
+//	str string - the string to capitalize.
+//
+// Returns:
+//
+//	string - the string with the first letter capitalized.
+//
+// Example:
+//
+//	{{ "hello world" | capitalize }} // Output: "Hello world"
+func (sr *StringsRegistry) Capitalize(str string) string {
+	for i, r := range str {
+		if unicode.IsLetter(r) {
+			return str[:i] + string(unicode.ToUpper(r)) + str[i+1:]
+		}
+	}
+
+	return str
+}
+
+// Uncapitalize converts the first letter of 'str' to lowercase.
+//
+// Parameters:
+//
+//	str string - the string to uncapitalize.
+//
+// Returns:
+//
+//	string - the string with the first letter in lowercase.
+//
+// Example:
+//
+//	{{ "Hello World" | uncapitalize }} // Output: "hello World"
+func (sr *StringsRegistry) Uncapitalize(str string) string {
+	for i, r := range str {
+		if unicode.IsLetter(r) {
+			return str[:i] + string(unicode.ToLower(r)) + str[i+1:]
+		}
+	}
+
+	return str
+}
+
 // Split divides 'orig' into a map of string parts using 'sep' as the separator.
 //
 // Parameters:
diff --git a/registry/strings/strings.go b/registry/strings/strings.go
index 3d5cfa7..6f3f2c2 100644
--- a/registry/strings/strings.go
+++ b/registry/strings/strings.go
@@ -127,6 +127,8 @@ func (sr *StringsRegistry) RegisterFunctions(funcsMap sprout.FunctionMap) error
 	sprout.AddFunction(funcsMap, "toTitleCase", sr.ToTitleCase)
 	sprout.AddFunction(funcsMap, "untitle", sr.Untitle)
 	sprout.AddFunction(funcsMap, "swapCase", sr.SwapCase)
+	sprout.AddFunction(funcsMap, "capitalize", sr.Capitalize)
+	sprout.AddFunction(funcsMap, "uncapitalize", sr.Uncapitalize)
 	sprout.AddFunction(funcsMap, "split", sr.Split)
 	sprout.AddFunction(funcsMap, "splitn", sr.Splitn)
 	sprout.AddFunction(funcsMap, "substr", sr.Substring)


Test patch:
diff --git a/registry/reflect/functions_test.go b/registry/reflect/functions_test.go
index 2b338a0..7be13cb 100644
--- a/registry/reflect/functions_test.go
+++ b/registry/reflect/functions_test.go
@@ -76,6 +76,29 @@ func TestKindOf(t *testing.T) {
 	pesticide.RunTestCases(t, reflect.NewRegistry(), tc)
 }
 
+func TestHasField(t *testing.T) {
+	type A struct {
+		Foo string
+	}
+	type B struct {
+		Bar string
+	}
+
+	var tc = []pesticide.TestCase{
+		{Name: "TestHasFieldStructPointerTrue", Input: `{{ .V |hasField "Foo" }}`, Expected: "true", Data: map[string]any{"V": &A{Foo: "bar"}}},
+		{Name: "TestHasFieldStructPointerFalse", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": &B{Bar: "boo"}}},
+		{Name: "TestHasFieldStructTrue", Input: `{{ .V |hasField "Foo" }}`, Expected: "true", Data: map[string]any{"V": A{Foo: "bar"}}},
+		{Name: "TestHasFieldStructFalse", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": B{Bar: "boo"}}},
+		{Name: "TestHasFieldInt", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": 123}},
+		{Name: "TestHasFieldMap", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": map[string]string{"Foo": "bar"}}},
+		{Name: "TestHasFieldSlice", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": []int{1, 2, 3}}},
+		{Name: "TestHasFieldString", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": "foobar"}},
+		{Name: "TestHasFieldNil", Input: `{{ .V |hasField "Foo" }}`, Expected: "false", Data: map[string]any{"V": nil}},
+	}
+
+	pesticide.RunTestCases(t, reflect.NewRegistry(), tc)
+}
+
 func TestDeepEqual(t *testing.T) {
 	var tc = []pesticide.TestCase{
 		{Name: "TestDeepEqualInt", Input: `{{deepEqual 42 42}}`, Expected: "true"},
diff --git a/registry/strings/functions_test.go b/registry/strings/functions_test.go
index 319a935..f20c9b0 100644
--- a/registry/strings/functions_test.go
+++ b/registry/strings/functions_test.go
@@ -424,6 +424,30 @@ func TestSwapCase(t *testing.T) {
 	pesticide.RunTestCases(t, strings.NewRegistry(), tc)
 }
 
+func TestCapitalize(t *testing.T) {
+	var tc = []pesticide.TestCase{
+		{Name: "TestEmpty", Input: `{{ "" | capitalize }}`, Expected: ""},
+		{Name: "CapitalizeAlreadyUpper", Input: `{{ "Foo Bar" | capitalize }}`, Expected: "Foo Bar"},
+		{Name: "CapitalizeWithSpace", Input: `{{ " fe bar" | capitalize }}`, Expected: " Fe bar"},
+		{Name: "CapitalizeWithNumber", Input: `{{ "123boo_bar" | capitalize }}`, Expected: "123Boo_bar"},
+		{Name: "CapitalizeWithUnderscore", Input: `{{ "boo_bar" | capitalize }}`, Expected: "Boo_bar"},
+	}
+
+	pesticide.RunTestCases(t, strings.NewRegistry(), tc)
+}
+
+func TestUncapitalize(t *testing.T) {
+	var tc = []pesticide.TestCase{
+		{Name: "TestEmpty", Input: `{{ "" | uncapitalize }}`, Expected: ""},
+		{Name: "UncapitalizeAlreadyLower", Input: `{{ "foo bar" | uncapitalize }}`, Expected: "foo bar"},
+		{Name: "UncapitalizeWithSpace", Input: `{{ " Foo bar" | uncapitalize }}`, Expected: " foo bar"},
+		{Name: "UncapitalizeWithNumber", Input: `{{ "123Boo_bar" | uncapitalize }}`, Expected: "123boo_bar"},
+		{Name: "UncapitalizeWithUnderscore", Input: `{{ "Boo_bar" | uncapitalize }}`, Expected: "boo_bar"},
+	}
+
+	pesticide.RunTestCases(t, strings.NewRegistry(), tc)
+}
+
 func TestSplit(t *testing.T) {
 	var tc = []pesticide.TestCase{
 		{Name: "TestEmpty", Input: `{{ $v := ("" | split "-") }}{{$v._0}}`, Expected: ""},