Skip to content

Commit

Permalink
[FEATURE-Issue-32] Enhanced HTML Templating Utilities for html/templa…
Browse files Browse the repository at this point in the history
…te (#38)

- Enhance Go’s html/template library by incorporating a comprehensive set of utility functions.
- This will enable more expressive and powerful templating, making it easier to handle inline conditionals, loops, and a variety of string, numeric, and date operations within templates.

Co-authored-by: Kashif Khan <[email protected]>
  • Loading branch information
mattjinks and kashifkhan0771 authored Nov 19, 2024
1 parent 1c951f5 commit 78b464d
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 3 deletions.
98 changes: 98 additions & 0 deletions readME.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,104 @@ func main() {
}
```


### Templates
Dynamic rendering of HTML and text templates.

**HTML Template Rendering**: Enhances Go's html/template library by incorporating a comprehensive set of utility functions making it easier to handle inline conditionals, loops, and a variety of string, numeric, logic, date, and debugging operations within templates.

Example:

```
package main
import (
"fmt"
"github.com/kashifkhan0771/utils/templates"
)
func main() {
htmlTemplate := `<h1>{{toUpper .Title}}</h1><p>{{.Content}}</p>`
data := map[string]interface{}{
"Title": "hello, world",
"Content": "This is a sample content for HTML template rendering.",
}
result, err := templates.RenderHTMLTemplate(htmlTemplate, data)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
// Output: <h1>HELLO, WORLD</h1><p>This is a sample content for HTML template rendering.</p>
}
```

**Text Template Rendering**: Enhances Go's text/template library by incorporating a comprehensive set of utility functions making it easier to handle a variety of string, numeric, logic, date, and debugging operations within templates.

Example:

```
package main
import (
"fmt"
"github.com/kashifkhan0771/utils/templates"
)
func main() {
textTemplate := `Name: {{.Name | toUpper}}, Age: {{add .Age 5}}`
data := map[string]interface{}{
"Name": "john doe",
"Age": 20,
}
result, err := templates.RenderText(textTemplate, data)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
// Output: Name: JOHN DOE, Age: 25
}
```

**Custom Functions**

The templates package offers a set of custom functions that can be directly used within templates to perform various operations.

_String Functions:_

- toUpper, toLower, title, contains, replace, trim, split, reverse, toString

_Date and Time Functions:_

- formatDate, now

_Arithmetic Functions:_

- add, sub, mul, div, mod

_Conditional and Logical Functions:_

- isNil, not

_Debugging Functions:_

- dump, typeOf

Example:

```
{{ "example text" | toUpper }} // Outputs: EXAMPLE TEXT
{{ formatDate now "2006-01-02" }} // Outputs the current date in YYYY-MM-DD format
{{ add 10 5 }} // Outputs: 15
{{ typeOf .SomeVariable }} // Outputs the type of .SomeVariable
```


# Contributions
Contributions to this project are welcome! If you would like to contribute, please feel free to open a PR.

Expand Down
12 changes: 9 additions & 3 deletions templates/custom_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package templates

import (
"fmt"
htmlTemplate "html/template"
"strings"
"text/template"
textTemplate "text/template"
"time"

strutils "github.com/kashifkhan0771/utils/strings"
)

// custom functions for templates
var customFuncsMap = template.FuncMap{
var customFuncsMap = textTemplate.FuncMap{
// string functions
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
Expand Down Expand Up @@ -40,9 +41,14 @@ var customFuncsMap = template.FuncMap{
// debugging functions
"dump": func(v interface{}) string { return fmt.Sprintf("%#v", v) },
"typeOf": func(v interface{}) string { return fmt.Sprintf("%T", v) },

// safe HTML rendering for trusted content
"safeHTML": func(s string) htmlTemplate.HTML {
return htmlTemplate.HTML(s)
},
}

// GetCustomFuncMap returns a map of custom functions
func GetCustomFuncMap() template.FuncMap {
func GetCustomFuncMap() textTemplate.FuncMap {
return customFuncsMap
}
21 changes: 21 additions & 0 deletions templates/html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package templates

import (
"bytes"
"html/template"
)

// RenderHTMLTemplate processes an HTML template with the provided data.
func RenderHTMLTemplate(tmpl string, data interface{}) (string, error) {
t, err := template.New("htmlTemplate").Funcs(GetCustomFuncMap()).Parse(tmpl)
if err != nil {
return "", err
}

var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", err
}

return buf.String(), nil
}
209 changes: 209 additions & 0 deletions templates/html_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package templates

import (
"html"
htmlTemplate "html/template"
"regexp"
"strings"
"testing"
"time"
)

func normalizeWhitespace(input string) string {
// Remove leading and trailing whitespace
input = strings.TrimSpace(input)

// Normalize whitespace by replacing multiple spaces and newlines with a single space
whitespace := regexp.MustCompile(`\s+`)
input = whitespace.ReplaceAllString(input, " ")

// Remove spaces before and after HTML tags to ensure consistent formatting
input = regexp.MustCompile(`\s*(<[^>]+>)\s*`).ReplaceAllString(input, "$1")

return input
}

var (
htmlTestTemplate1 = `<!DOCTYPE html><html><body>Welcome {{toUpper .Name}}! Today is {{formatDate .Date "2006-01-02"}}.</body></html>`
htmlWant1 = `<!DOCTYPE html><html><body>Welcome ALICE! Today is 2024-10-01.</body></html>`

htmlTestTemplate2 = `<!DOCTYPE html><html><body>
{{- $upper := toUpper "hello" -}}
{{- $lower := toLower "WORLD" -}}
{{- $title := title "hello world" -}}
{{- $contains := contains "hello world" "world" -}}
{{- $replace := replace "go gophers" "go" "GoLang" -}}
{{- $trim := trim " trimmed " -}}
{{- $split := index (split "one,two,three" ",") 1 -}}
{{- $reverse := reverse "abcde" -}}
Uppercase: {{$upper}}<br>
Lowercase: {{$lower}}<br>
Title Case: {{$title}}<br>
Contains 'world': {{$contains}}<br>
Replace 'go' with 'GoLang': {{$replace}}<br>
Trimmed: {{$trim}}<br>
Split Result [1]: {{$split}}<br>
Reversed: {{$reverse}}
</body></html>`
htmlWant2 = `<!DOCTYPE html><html><body>
Uppercase: HELLO<br>
Lowercase: world<br>
Title Case: Hello World<br>
Contains 'world': true<br>
Replace 'go' with 'GoLang': GoLang GoLangphers<br>
Trimmed: trimmed<br>
Split Result [1]: two<br>
Reversed: edcba
</body></html>`

htmlTestTemplate3 = `<!DOCTYPE html><html><body>
{{- $sum := add 1 4 -}}
{{- $sub := sub 4 1 -}}
{{- $mul := mul 2 2 -}}
{{- $div := div 2 2 -}}
{{- $mod := mod 3 2 -}}
Addition: {{$sum}}<br>
Subtraction: {{$sub}}<br>
Multiplication: {{$mul}}<br>
Division: {{$div}}<br>
Mod: {{$mod}}
</body></html>`
htmlWant3 = `<!DOCTYPE html><html><body>
Addition: 5<br>
Subtraction: 3<br>
Multiplication: 4<br>
Division: 1<br>
Mod: 1
</body></html>`

htmlTestTemplate4 = `<!DOCTYPE html><html><body>
{{- $isNil := isNil .NilValue -}}
{{- $notNil := isNil .NotNilValue -}}
{{- $notTrue := not true -}}
Is Nil: {{$isNil}}<br>
Is Nil: {{$notNil}}<br>
Not True: {{$notTrue}}
</body></html>`
htmlWant4 = `<!DOCTYPE html><html><body>
Is Nil: true<br>
Is Nil: false<br>
Not True: false
</body></html>`

htmlTestTemplate5 = `<!DOCTYPE html><html><body>
{{- $dumpValue := dump .SampleMap -}}
{{- $typeOfValue := typeOf .SampleMap -}}
Dump: {{$dumpValue}}<br>
Type Of: {{$typeOfValue}}
</body></html>`
htmlWant5 = `<!DOCTYPE html><html><body>
Dump: map[string]int{"a":1, "b":2}<br>
Type Of: map[string]int
</body></html>`
)

func TestRenderHTML(t *testing.T) {
type args struct {
tmpl string
data interface{}
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "success - simple HTML",
args: args{
tmpl: htmlTestTemplate1,
data: struct {
Name string
Date time.Time
}{
Name: "alice",
Date: time.Date(2024, 10, 1, 0, 0, 0, 0, time.UTC),
},
},
want: htmlWant1,
wantErr: false,
},
{
name: "success - string funcs in HTML",
args: args{
tmpl: normalizeWhitespace(htmlTestTemplate2),
data: nil,
},
want: normalizeWhitespace(htmlWant2),
wantErr: false,
},
{
name: "success - numeric and arithmetic funcs in HTML",
args: args{
tmpl: normalizeWhitespace(htmlTestTemplate3),
data: nil,
},
want: normalizeWhitespace(htmlWant3),
wantErr: false,
},
{
name: "success - conditional and logical funcs in HTML",
args: args{
tmpl: normalizeWhitespace(htmlTestTemplate4),
data: struct {
NilValue interface{}
NotNilValue interface{}
}{
NilValue: nil,
NotNilValue: "example",
},
},
want: normalizeWhitespace(htmlWant4),
wantErr: false,
},
{
name: "success - debugging funcs in HTML",
args: args{
tmpl: normalizeWhitespace(htmlTestTemplate5),
data: struct {
SampleMap map[string]int
}{
SampleMap: map[string]int{"a": 1, "b": 2},
},
},
want: normalizeWhitespace(htmlWant5),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpl, err := htmlTemplate.New("htmlTestTemplate").Funcs(GetCustomFuncMap()).Parse(tt.args.tmpl)
if err != nil {
t.Fatalf("failed to parse template: %v", err)
}
var sb strings.Builder
err = tmpl.Execute(&sb, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := sb.String()

//Trim whitespace for comparison
gotTrimmed := strings.TrimSpace(got)
wantTrimmed := strings.TrimSpace(tt.want)

// Handle HTML-escaped characters
gotUnescaped := html.UnescapeString(gotTrimmed)
wantUnescaped := html.UnescapeString(wantTrimmed)

if gotUnescaped != wantUnescaped {
t.Errorf("Execute() = %v, want %v", gotUnescaped, wantUnescaped)
}
})
}
}

0 comments on commit 78b464d

Please sign in to comment.