diff --git a/controllers/themedemo.go b/controllers/themedemo.go index 03c55c1..85446e6 100644 --- a/controllers/themedemo.go +++ b/controllers/themedemo.go @@ -9,6 +9,7 @@ import ( "math/rand/v2" "os" "strconv" + "time" "git.smarteching.com/goffee/core" "git.smarteching.com/goffee/core/template/components" @@ -861,3 +862,133 @@ func getPaginatedPageViews(values [][]components.ContentTableTD, limit int, offs return values[offset:end] // Slice the array } + +// Custom Templates functions +func TemplatesFunctions(c *core.Context) *core.Response { + + // check if template engine is enabled + TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") + if TemplateEnableStr == "" { + TemplateEnableStr = "false" + } + TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) + + if TemplateEnable { + + tmplData := SamplePageData() + return c.Response.Template("custom_templates_functions.html", tmplData) + + } else { + + message := "{\"message\": \"Error, template not enabled\"}" + return c.Response.Json(message) + + } + +} + +// Author represents the writer of an article to show the custom functions +type Author struct { + Name string + Avatar string + Bio string +} + +// Article represents a blog post or news entry used to test template helpers to show the custom functions +type Article struct { + Title string + Slug string + Excerpt string + Body string + Tags []string + PublishedAt time.Time + UpdatedAt time.Time + Author Author + Views int + Price float64 + Featured bool + Subtitle string // intentionally left empty on some entries to test coalesce/defaultVal +} + +// PageData is the top-level context passed to the HTML template to show the custom functions +type PageData struct { + SiteTitle string + Articles []Article +} + +// SamplePageData returns a populated PageData ready to be rendered by the template. +func SamplePageData() PageData { + return PageData{ + SiteTitle: "go/template lab", + Articles: []Article{ + { + Title: "getting started with go templates", + Slug: "getting-started-go-templates", + Excerpt: "Go's html/template package is both powerful and safe by default. In this article we explore how to extend it with custom FuncMap helpers that bring it closer to the expressiveness of Liquid or Twig, without sacrificing any of the security guarantees.", + Tags: []string{"go", "templates", "web", "backend"}, + PublishedAt: time.Now().Add(-3 * 24 * time.Hour), + UpdatedAt: time.Now().Add(-1 * 24 * time.Hour), + Views: 14200, + Price: 0, + Featured: true, + Subtitle: "", + Author: Author{ + Name: "marina voss", + Avatar: "MV", + Bio: "Senior backend engineer focused on developer tooling and observability.", + }, + }, + { + Title: "building a blog engine in go", + Slug: "blog-engine-go", + Excerpt: "We walk through building a minimal but complete blog engine using only the Go standard library: routing with net/http, persistence with database/sql, and rendering with html/template.", + Tags: []string{"go", "blog", "sqlite"}, + PublishedAt: time.Now().Add(-10 * 24 * time.Hour), + UpdatedAt: time.Now().Add(-10 * 24 * time.Hour), + Views: 8750, + Price: 9.99, + Featured: false, + Subtitle: "A zero-dependency approach", + Author: Author{ + Name: "rafael okonkwo", + Avatar: "RO", + Bio: "Full-stack engineer and open-source contributor. Writes about Go and distributed systems.", + }, + }, + { + Title: "concurrency patterns you should know", + Slug: "concurrency-patterns-go", + Excerpt: "Goroutines are cheap, but misusing them is expensive. This deep-dive covers fan-out/fan-in, pipelines, semaphores, and error group patterns with real production examples.", + Tags: []string{"go", "concurrency", "goroutines", "advanced"}, + PublishedAt: time.Now().Add(-45 * 24 * time.Hour), + UpdatedAt: time.Now().Add(-40 * 24 * time.Hour), + Views: 31400, + Price: 14.99, + Featured: true, + Subtitle: "", + Author: Author{ + Name: "selin çelik", + Avatar: "SÇ", + Bio: "Systems programmer. Previously at Cloudflare. Loves writing about the internals of things.", + }, + }, + { + Title: "understanding go interfaces", + Slug: "understanding-go-interfaces", + Excerpt: "Interfaces in Go are implicit and structural, which makes them both elegant and occasionally surprising. We look at how the runtime dispatches method calls and how to design composable interfaces.", + Tags: []string{"go", "interfaces", "design"}, + PublishedAt: time.Now().Add(-2 * time.Hour), + UpdatedAt: time.Now().Add(-1 * time.Hour), + Views: 420, + Price: 0, + Featured: false, + Subtitle: "Implicit, structural, and powerful", + Author: Author{ + Name: "marina voss", + Avatar: "MV", + Bio: "Senior backend engineer focused on developer tooling and observability.", + }, + }, + }, + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index c51f815..21103fe 100644 --- a/go.mod +++ b/go.mod @@ -21,9 +21,11 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect git.smarteching.com/zeni/go-chart/v2 v2.1.4 // indirect + git.smarteching.com/zeni/go-charts/v2 v2.6.11 // indirect github.com/SparkPost/gosparkpost v0.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect diff --git a/go.sum b/go.sum index 76d68f0..2bf5036 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ git.smarteching.com/goffee/core v1.9.6 h1:GY1EXqbmBEWZAVrl3q22Izb6aXhQzFVQBv2hWh git.smarteching.com/goffee/core v1.9.6/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= +git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI= +git.smarteching.com/zeni/go-charts/v2 v2.6.11/go.mod h1:3OpRPSXg7Qx4zcgsmwsC9ZFB9/wAkGSbnXf1wIbHYCg= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -21,6 +23,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= diff --git a/routes.go b/routes.go index f8d7fec..dc1799c 100644 --- a/routes.go +++ b/routes.go @@ -60,4 +60,6 @@ func registerRoutes() { controller.Get("/appsession", controllers.AppSession) controller.Post("/appsession", controllers.AppSession) + + controller.Get("/templatesfunc", controllers.TemplatesFunctions) } diff --git a/storage/templates/custom_templates_functions.html b/storage/templates/custom_templates_functions.html new file mode 100644 index 0000000..6e0c357 --- /dev/null +++ b/storage/templates/custom_templates_functions.html @@ -0,0 +1,471 @@ + + + + + + {{.SiteTitle}} — template lab + + + + + + + +
+
+

{{.SiteTitle}}

+

Template helpers
in action

+
+

22 helpers registered
across 6 groups

+
+ + + +
String helpers
+ +{{$first := first .Articles}} + +
+ +
+

capitalize capitalize

+

{{capitalize $first.Title}}

+

input: "{{$first.Title}}"

+
+ +
+

truncate truncate

+

{{$first.Excerpt | truncate 80}}

+

capped at 80 chars

+
+ +
+

prepend prepend

+

{{prepend $first.Slug "/articles/"}}

+

prepended "/articles/" to slug

+
+ +
+

strAppend strAppend

+

{{strAppend $first.Slug ".html"}}

+

appended ".html" to slug

+
+ +
+

split → join split join

+ {{$parts := split "go,templates,funcmap" ","}} +

{{join $parts " · "}}

+

split on "," then joined with " · "

+
+ +
+ + + +
Number & Date helpers
+ +
+ +
+

fmtNumber — int fmtNumber

+

{{fmtNumber $first.Views}}

+

raw value: {{$first.Views}}

+
+ + {{$paid := index .Articles 2}} +
+

fmtNumber — float fmtNumber

+

$ {{fmtNumber $paid.Price}}

+

raw value: {{$paid.Price}}

+
+ +
+

fmtDate "short" fmtDate

+

{{fmtDate $first.PublishedAt "short"}}

+

layout: "02 Jan 2006"

+
+ +
+

fmtDate "long" fmtDate

+

{{fmtDate $first.PublishedAt "long"}}

+

layout: "02 January 2006"

+
+ +
+

fmtDate "iso" fmtDate

+

{{fmtDate $first.PublishedAt "iso"}}

+

layout: "2006-01-02"

+
+ +
+

timeAgo timeAgo

+ {{range .Articles}} +

{{timeAgo .PublishedAt}} — {{fmtDate .PublishedAt "short"}}

+ {{end}} +
+ +
+ + + +
Collection helpers
+ +
+ +
+

first first

+ {{with first .Articles}} +

{{capitalize .Title}}

+

first article in the list

+ {{end}} +
+ +
+

last last

+ {{with last .Articles}} +

{{capitalize .Title}}

+

last article in the list

+ {{end}} +
+ +
+

sliceOf 0–2 sliceOf

+ {{range sliceOf .Articles 0 2}} +

— {{capitalize .Title}}

+ {{end}} +

only first 2 of {{len .Articles}} articles

+
+ +
+

contains — slice contains

+ {{if contains $first.Tags "go"}} +

✓ has tag "go"

+ {{else}} +

✗ missing tag "go"

+ {{end}} +

tags: {{join $first.Tags ", "}}

+
+ +
+

contains — string contains

+ {{if contains $first.Excerpt "FuncMap"}} +

✓ excerpt mentions "FuncMap"

+ {{else}} +

✗ not found

+ {{end}} +

substring search on .Excerpt

+
+ +
+

join join

+

{{join $first.Tags " / "}}

+

tags joined with " / "

+
+ +
+ + + +
Logic helpers
+ + + + + + + + + + + + {{range .Articles}} + + + + + + + + + + + + + + + + + + + {{end}} + +
HelperArticleInputOutput
defaultVal{{.Title | capitalize | truncate 30}}.Subtitle ({{if .Subtitle}}"{{.Subtitle}}"{{else}}empty{{end}}){{defaultVal "No subtitle" .Subtitle}}
ternary{{capitalize .Title | truncate 30}}.Featured = {{.Featured}}{{ternary "⭐ featured" "regular" .Featured}}
coalesce{{capitalize .Title | truncate 30}}.Subtitle → .Title{{coalesce .Subtitle .Title}}
+ + + +
Full article cards — all helpers combined
+ +
+ {{range .Articles}} +
+
+
+ {{if .Featured}}featured{{end}} + {{range .Tags}}{{.}}{{end}} +
+ +

{{capitalize .Title}}

+ + {{with coalesce .Subtitle ""}} +

{{.}}

+ {{end}} + +

{{.Excerpt | truncate 160}}

+ + + {{prepend .Slug "/articles/"}} → + + +
+
{{.Author.Avatar}}
+
+
{{capitalize .Author.Name}}
+ +
+
+
+ +
+
+ {{fmtNumber .Views}} + views +
+ {{if gt .Price 0.0}} + $ {{fmtNumber .Price}} + {{else}} + free + {{end}} +
+
+ {{end}} +
+ + + + + + + \ No newline at end of file