From 78b464d64c2f4b0f6281f2bbf0da0e958f38f37a Mon Sep 17 00:00:00 2001 From: Matthew Jinks Date: Tue, 19 Nov 2024 00:42:34 -0500 Subject: [PATCH] [FEATURE-Issue-32] Enhanced HTML Templating Utilities for html/template (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 <70996046+kashifkhan0771@users.noreply.github.com> --- readME.md | 98 ++++++++++++++++++ templates/custom_funcs.go | 12 ++- templates/html.go | 21 ++++ templates/html_test.go | 209 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 templates/html.go create mode 100644 templates/html_test.go diff --git a/readME.md b/readME.md index 0403071..a5740c1 100644 --- a/readME.md +++ b/readME.md @@ -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 := `

{{toUpper .Title}}

{{.Content}}

` + 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:

HELLO, WORLD

This is a sample content for HTML template rendering.

+} +``` + +**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. diff --git a/templates/custom_funcs.go b/templates/custom_funcs.go index 769ba80..f923e66 100644 --- a/templates/custom_funcs.go +++ b/templates/custom_funcs.go @@ -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, @@ -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 } diff --git a/templates/html.go b/templates/html.go new file mode 100644 index 0000000..e2ddfac --- /dev/null +++ b/templates/html.go @@ -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 +} diff --git a/templates/html_test.go b/templates/html_test.go new file mode 100644 index 0000000..6dca50e --- /dev/null +++ b/templates/html_test.go @@ -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 = `Welcome {{toUpper .Name}}! Today is {{formatDate .Date "2006-01-02"}}.` + htmlWant1 = `Welcome ALICE! Today is 2024-10-01.` + + htmlTestTemplate2 = ` + {{- $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}}
+ Lowercase: {{$lower}}
+ Title Case: {{$title}}
+ Contains 'world': {{$contains}}
+ Replace 'go' with 'GoLang': {{$replace}}
+ Trimmed: {{$trim}}
+ Split Result [1]: {{$split}}
+ Reversed: {{$reverse}} + ` + htmlWant2 = ` + Uppercase: HELLO
+ Lowercase: world
+ Title Case: Hello World
+ Contains 'world': true
+ Replace 'go' with 'GoLang': GoLang GoLangphers
+ Trimmed: trimmed
+ Split Result [1]: two
+ Reversed: edcba + ` + + htmlTestTemplate3 = ` + {{- $sum := add 1 4 -}} + {{- $sub := sub 4 1 -}} + {{- $mul := mul 2 2 -}} + {{- $div := div 2 2 -}} + {{- $mod := mod 3 2 -}} + + Addition: {{$sum}}
+ Subtraction: {{$sub}}
+ Multiplication: {{$mul}}
+ Division: {{$div}}
+ Mod: {{$mod}} + ` + htmlWant3 = ` + Addition: 5
+ Subtraction: 3
+ Multiplication: 4
+ Division: 1
+ Mod: 1 + ` + + htmlTestTemplate4 = ` + {{- $isNil := isNil .NilValue -}} + {{- $notNil := isNil .NotNilValue -}} + {{- $notTrue := not true -}} + + Is Nil: {{$isNil}}
+ Is Nil: {{$notNil}}
+ Not True: {{$notTrue}} + ` + htmlWant4 = ` + Is Nil: true
+ Is Nil: false
+ Not True: false + ` + + htmlTestTemplate5 = ` + {{- $dumpValue := dump .SampleMap -}} + {{- $typeOfValue := typeOf .SampleMap -}} + + Dump: {{$dumpValue}}
+ Type Of: {{$typeOfValue}} + ` + htmlWant5 = ` + Dump: map[string]int{"a":1, "b":2}
+ Type Of: map[string]int + ` +) + +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) + } + }) + } +}