3 Commits

Author SHA1 Message Date
brent saner
834395c050 v1.15.2
ADDED:
* Better docs for remap
* Added returner convenience funcs for remap

FIXED:
* Proper resliced remap.ReMap.MapString
2026-01-06 02:54:38 -05:00
brent saner
ef56898d6b v1.15.1
ADDED:
* timex, for some floaty-UNIX-y things
2025-12-23 18:57:28 -05:00
brent saner
006cf39fa1 v1.15.0
ADDED:
* tplx, for one-shotting/shortcutting templating
2025-12-23 17:26:50 -05:00
15 changed files with 870 additions and 16 deletions

1
go.sum
View File

@@ -13,3 +13,4 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
r00t2.io/sysutils v1.15.0 h1:FSnREfbXDhBQEO7LMpnRQeKlPshozxk9XHw3YgWRgRg=
r00t2.io/sysutils v1.15.0/go.mod h1:28qB0074EIRQ8Sy/ybaA5jC3qA32iW2aYLkMCRhyAFM=
r00t2.io/sysutils v1.15.1/go.mod h1:T0iOnaZaSG5NE1hbXTqojRZc0ia/u8TB73lV7zhMz58=

View File

@@ -1,4 +1,12 @@
/*
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
Package remap provides convenience functions around regular expressions,
primarily offering maps for named capture groups.
It offers convenience equivalents of the following:
* [regexp.Compile] ([Compile])
* [regexp.CompilePOSIX] ([CompilePOSIX])
* [regexp.MustCompile] ([MustCompile])
* [regexp.MustCompilePOSIX] ([MustCompilePOSIX])
*/
package remap

113
remap/funcs.go Normal file
View File

@@ -0,0 +1,113 @@
package remap
import (
"regexp"
)
/*
Compile is a convenience shorthand for:
var err error
var r *remap.ReMap = new(remap.ReMap)
if r.Regexp, err = regexp.Compile(expr); err != nil {
// ...
}
It corresponds to [regexp.Compile].
*/
func Compile(expr string) (r *ReMap, err error) {
var p *regexp.Regexp
if p, err = regexp.Compile(expr); err != nil {
return
}
r = &ReMap{
Regexp: p,
}
return
}
/*
CompilePOSIX is a convenience shorthand for:
var err error
var r *remap.ReMap = new(remap.ReMap)
if r.Regexp, err = regexp.CompilePOSIX(expr); err != nil {
// ...
}
It corresponds to [regexp.CompilePOSIX].
*/
func CompilePOSIX(expr string) (r *ReMap, err error) {
var p *regexp.Regexp
if p, err = regexp.CompilePOSIX(expr); err != nil {
return
}
r = &ReMap{
Regexp: p,
}
return
}
/*
MustCompile is a convenience shorthand for:
var r *remap.ReMap = &remap.ReMap{
Regexp: regexp.MustCompile(expr),
}
It corresponds to [regexp.MustCompile].
*/
func MustCompile(expr string) (r *ReMap) {
var err error
var p *regexp.Regexp
// We panic ourselves instead of wrapping regexp.MustCompile.
// Makes debuggers a little more explicit.
if p, err = regexp.Compile(expr); err != nil {
panic(err)
}
r = &ReMap{
Regexp: p,
}
return
}
/*
MustCompilePOSIX is a convenience shorthand for:
var r *remap.ReMap = &remap.ReMap{
Regexp: regexp.MustCompilePOSIX(expr),
}
It corresponds to [regexp.MustCompilePOSIX].
*/
func MustCompilePOSIX(expr string) (r *ReMap) {
var err error
var p *regexp.Regexp
// We panic ourselves instead of wrapping regexp.MustCompilePOSIX.
// Makes debuggers a little more explicit.
if p, err = regexp.CompilePOSIX(expr); err != nil {
panic(err)
}
r = &ReMap{
Regexp: p,
}
return
}

View File

@@ -5,6 +5,8 @@ Map returns a map[string][]<match bytes> for regexes with named capture groups m
Note that this supports non-unique group names; [regexp.Regexp] allows for patterns with multiple groups
using the same group name (though your IDE might complain; I know GoLand does).
It will panic if the embedded [regexp.Regexp] is nil.
Each match for each group is in a slice keyed under that group name, with that slice
ordered by the indexing done by the regex match itself.
@@ -87,7 +89,7 @@ In detail, matches and/or its values may be nil or empty under the following con
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == [][]byte{nil[, nil...]})
(matches[<group name>] == [][]byte{nil[, nil, ...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
@@ -109,7 +111,7 @@ func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (m
return
}
names = r.Regexp.SubexpNames()
names = r.Regexp.SubexpNames()[:]
matchBytes = r.Regexp.FindSubmatch(b)
if matchBytes == nil {
@@ -204,13 +206,15 @@ func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (m
}
/*
MapString is exactly like ReMap.Map(), but operates on (and returns) strings instead.
MapString is exactly like [ReMap.Map], but operates on (and returns) strings instead.
(matches will always be nil if s == “.)
A small deviation, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
It will panic if the embedded [regexp.Regexp] is nil.
A small deviation and caveat, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
This unfortunately *does not provide any indication* if an empty string positively matched the pattern (a "hit") or if it was simply
not matched at all (a "miss"). If you need definitive determination between the two conditions, it is instead recommended to either
*not* use inclNoMatchStrict or to use ReMap.Map() instead and convert any non-nil values to strings after.
*not* use inclNoMatchStrict or to use [ReMap.Map] instead and convert any non-nil values to strings after.
Particularly:
@@ -233,7 +237,7 @@ is provided but s does not match then matches will be:
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of eempty string values explicitly matching the number of times the group name is specified
a slice of empty string values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
@@ -290,8 +294,8 @@ In detail, matches and/or its values may be nil or empty under the following con
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == []string{""[, ""...]})
THEN matches[<group name>] is defined and non-nil, but populated with placeholder strings
(matches[<group name>] == []string{""[, "", ...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
@@ -334,7 +338,8 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
return
}
/*
I'm not entirely sure how serious they are about "the slice should not be modified"...
I'm not entirely sure how serious they are about
"the slice should not be modified"...
DO NOT sort or dedupe `names`! If the same name for groups is duplicated,
it will be duplicated here in proper order and the ordering is tied to
@@ -351,7 +356,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
return
}
if names == nil || len(names) <= 1 {
if names == nil || len(names) == 0 || len(names) == 1 {
/*
No named capture groups;
technically only the last condition would be the case,
@@ -363,6 +368,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
}
return
}
names = names[1:]
if len(matchIndices) == 0 || len(matchIndices) == 1 {
/*
@@ -385,15 +391,15 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
}
return
}
matchIndices = matchIndices[2:]
/*
A reslice of `matchIndices` could technically start at 2 (as long as `names` is sliced [1:])
A reslice of `matchIndices` starts at 2 (as long as `names` is sliced [1:])
because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
and the first pair is the entire pattern match (un-resliced names[0]).
Thus the len(matchIndices) == 2*len(names), *even* if you
Thus the len(matchIndices) == 2*len(names), *even* if you reslice.
Keep in mind that since the first element of names is removed,
the first pair here is skipped.
This provides a bit more consistent readability, though.
we reslices matchIndices as well (above).
*/
idxChunks = make([][]int, len(names))
chunkIdx = 0
@@ -411,6 +417,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
// group did not match
chunkIndices = nil
} else {
// single character
if chunkIndices[0] == chunkIndices[1] {
chunkIndices = []int{chunkIndices[0]}
} else {
@@ -432,6 +439,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
(which is either an *unnamed* capture group
OR the first element in `names`, which is always
the entire match).
(We reslice out the latter.)
*/
if grpNm == "" {
continue

279
remap/funcs_remap_test.go Normal file
View File

@@ -0,0 +1,279 @@
package remap
import (
`fmt`
`reflect`
`regexp`
"testing"
)
type (
testMatcher struct {
Nm string
S string
M *ReMap
Expected map[string][][]byte
ExpectedStr map[string][]string
ParamInclNoMatch bool
ParamInclNoMatchStrict bool
ParamInclMustMatch bool
}
)
func TestRemap(t *testing.T) {
var matches map[string][][]byte
for midx, m := range []testMatcher{
testMatcher{
Nm: "No matches",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: nil,
},
testMatcher{
Nm: "Single mid match",
S: "This contains a single match in the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
Expected: map[string][][]byte{
"g1": [][]byte{[]byte("match")},
},
},
testMatcher{
Nm: "multi mid match",
S: "This contains a single match and another match in the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
Expected: map[string][][]byte{
"g1": [][]byte{
[]byte("match"),
[]byte("match"),
},
},
},
testMatcher{
Nm: "line match",
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
Expected: map[string][][]byte{
"g1": [][]byte{
[]byte("match"),
},
},
},
testMatcher{
Nm: "multiline match",
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
Expected: map[string][][]byte{
"g1": [][]byte{
[]byte("match"),
[]byte("match"),
},
},
},
} {
matches = m.M.Map([]byte(m.S), false, false, false)
t.Logf(
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
midx+1,
m.S,
m.M.Regexp.String(),
testBmapToStrMap(matches),
)
if !reflect.DeepEqual(matches, m.Expected) {
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.Expected, matches)
}
}
}
func TestRemapParams(t *testing.T) {
var matches map[string][][]byte
for midx, m := range []testMatcher{
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: nil,
ParamInclNoMatch: false,
ParamInclNoMatchStrict: false,
ParamInclMustMatch: false,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: nil,
ParamInclNoMatch: false,
ParamInclNoMatchStrict: true,
ParamInclMustMatch: false,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: nil,
ParamInclNoMatch: false,
ParamInclNoMatchStrict: true,
ParamInclMustMatch: true,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: nil,
ParamInclNoMatch: false,
ParamInclNoMatchStrict: false,
ParamInclMustMatch: true,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: make(map[string][][]byte),
ParamInclNoMatch: true,
ParamInclNoMatchStrict: false,
ParamInclMustMatch: false,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: make(map[string][][]byte),
ParamInclNoMatch: true,
ParamInclNoMatchStrict: true,
ParamInclMustMatch: false,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: make(map[string][][]byte),
ParamInclNoMatch: true,
ParamInclNoMatchStrict: true,
ParamInclMustMatch: true,
},
testMatcher{
Nm: "",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
Expected: make(map[string][][]byte),
ParamInclNoMatch: true,
ParamInclNoMatchStrict: false,
ParamInclMustMatch: true,
},
} {
matches = m.M.Map([]byte(m.S), m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch)
t.Logf(
"%d: %v/%v/%v: %#v\n",
midx+1, m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch, matches,
)
if !reflect.DeepEqual(matches, m.Expected) {
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
}
}
}
func TestRemapString(t *testing.T) {
var matches map[string][]string
for midx, m := range []testMatcher{
testMatcher{
Nm: "No matches",
S: "this is a test",
M: &ReMap{regexp.MustCompile(``)},
ExpectedStr: nil,
},
testMatcher{
Nm: "Single mid match",
S: "This contains a single match in the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
ExpectedStr: map[string][]string{
"g1": []string{"match"},
},
},
testMatcher{
Nm: "multi mid match",
S: "This contains a single match and another match in the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
ExpectedStr: map[string][]string{
"g1": []string{
"match",
"match",
},
},
},
testMatcher{
Nm: "line match",
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
ExpectedStr: map[string][]string{
"g1": []string{
"match",
},
},
},
testMatcher{
Nm: "multiline match",
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
ExpectedStr: map[string][]string{
"g1": []string{
"match",
"match",
},
},
},
} {
matches = m.M.MapString(m.S, false, false, false)
t.Logf(
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
midx+1,
m.S,
m.M.Regexp.String(),
testSmapToStrMap(matches),
)
if !reflect.DeepEqual(matches, m.ExpectedStr) {
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
}
}
}
func testBmapToStrMap(bmap map[string][][]byte) (s string) {
if bmap == nil {
return
}
s = "\n"
for k, v := range bmap {
s += fmt.Sprintf("\t%s\n", k)
for _, i := range v {
s += fmt.Sprintf("\t\t%s\n", string(i))
}
}
return
}
func testSmapToStrMap(smap map[string][]string) (s string) {
if smap == nil {
return
}
s = "\n"
for k, v := range smap {
s += fmt.Sprintf("\t%s\n", k)
for _, i := range v {
s += fmt.Sprintf("\t\t%s\n", i)
}
}
return
}

View File

@@ -5,7 +5,7 @@ import (
)
type (
// ReMap provides some map-related functions around a regexp.Regexp.
// ReMap provides some map-related functions around a [regexp.Regexp].
ReMap struct {
*regexp.Regexp
}

4
timex/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package timex provides some handy [time]-related functions.
*/
package timex

35
timex/funcs.go Normal file
View File

@@ -0,0 +1,35 @@
package timex
import (
`time`
)
/*
F64Seconds returns [time.Time] `t` as a 64-bit float of <seconds>.<nanoseconds>
(where <nanoseconds> is the number of nanoseconds since <seconds>,
and <seconds> is the number of seconds since the UNIX epoch).
This can be used to represent a UNIX Epoch timestamp as seconds but with nanosecond precision.
*/
func F64Seconds(t time.Time) (f64 float64) {
return F64Nanoseconds(t) / float64(time.Second)
}
/*
F64Milliseconds is like [F64Seconds] but with a millisecond integer.
*/
func F64Milliseconds(t time.Time) (f64 float64) {
return F64Nanoseconds(t) / float64(time.Millisecond)
}
/*
F64Microseconds is like [F64Seconds] but with a microsecond integer.
*/
func F64Microseconds(t time.Time) (f64 float64) {
return F64Nanoseconds(t) / float64(time.Microsecond)
}
// F64Nanoseconds returns [time.Time.UnixNano] as a float64.
func F64Nanoseconds(t time.Time) (f64 float64) {
return float64(t.UnixNano())
}

30
timex/funcs_test.go Normal file
View File

@@ -0,0 +1,30 @@
package timex
import (
"testing"
`time`
)
func TestF64(t *testing.T) {
var tmNano float64 = 1766533329999999999
var tmSeconds float64 = 1766533329.999999999
var tmMilli float64 = 1766533329999.999999
var tmMicro float64 = 1766533329999999.999
// 2025-12-23 23:42:09.999999999 +0000 UTC
var tm time.Time = time.Unix(1766533329, int64(time.Second-1))
if F64Seconds(tm) != tmSeconds {
t.Fatalf("Failed seconds: %f != %f", F64Seconds(tm), tmSeconds)
}
if F64Milliseconds(tm) != tmMilli {
t.Fatalf("Failed milliseconds: %f != %f", F64Milliseconds(tm), tmMilli)
}
if F64Microseconds(tm) != tmMicro {
t.Fatalf("Failed microseconds: %f != %f", F64Microseconds(tm), tmMicro)
}
if F64Nanoseconds(tm) != tmNano {
t.Fatalf("Failed nanoseconds: %f != %f", F64Nanoseconds(tm), tmNano)
}
}

6
tplx/consts.go Normal file
View File

@@ -0,0 +1,6 @@
package tplx
const (
TplTypeText tplType = iota
TplTypeHtml
)

4
tplx/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package tplx provides some "shortcuts" to [text/template] and [html/template] rendering.
*/
package tplx

9
tplx/errs.go Normal file
View File

@@ -0,0 +1,9 @@
package tplx
import (
`errors`
)
var (
ErrInvalidTplType = errors.New("unknown/invalid template type")
)

235
tplx/funcs.go Normal file
View File

@@ -0,0 +1,235 @@
package tplx
import (
`bytes`
htmlTpl `html/template`
txtTpl `text/template`
)
// MustTplStrToStr wraps [TplStrToStr] but will panic on a non-nil error instead of returning it.
func MustTplStrToStr(tplStr string, typ tplType, obj any) (s string) {
var err error
if s, err = TplStrToStr(tplStr, typ, obj); err != nil {
panic(err)
}
return
}
// MustTplToStr wraps [TplToStr] but will panic on error instead of returning it.
func MustTplToStr[T Template](tpl T, obj any) (s string) {
var err error
if s, err = TplToStr(tpl, obj); err != nil {
panic(err)
}
return
}
// MustTplToStrWith wraps [TplToStrWith] but will panic on error instead of returning it.
func MustTplToStrWith[T Template](tpl T, tplNm string, obj any) (s string) {
var err error
if s, err = TplToStrWith(tpl, tplNm, obj); err != nil {
panic(err)
}
return
}
/*
TplStrToStr takes in a template string, a template type (see i.e. [TplTypeText], [TplTypeHtml]),
and an object and renders to a string.
This is obviously quite inflexible - there's no way to provide a [text/template.FuncMap]/[html/template.FuncMap],
for instance, but if more advanced template features aren't needed then this might just do the trick.
If you need something more flexible, see [TplToStr] instead.
*/
func TplStrToStr(tplStr string, typ tplType, obj any) (out string, err error) {
var ttpl *txtTpl.Template
var htpl *htmlTpl.Template
var buf *bytes.Buffer = new(bytes.Buffer)
switch typ {
case TplTypeText:
if ttpl, err = txtTpl.New("").Parse(tplStr); err != nil {
return
}
if err = ttpl.Execute(buf, obj); err != nil {
return
}
case TplTypeHtml:
if htpl, err = htmlTpl.New("").Parse(tplStr); err != nil {
return
}
if err = htpl.Execute(buf, obj); err != nil {
return
}
default:
err = ErrInvalidTplType
return
}
out = buf.String()
return
}
/*
TplToStr takes in an [html/template] or [text/template] and an object and executes it.
PLEASE NOTE that it is expected that `tpl` has already had at least one template string `.Parse()`'d in.
If you haven't used generics in Golang yet, this function would be used via something like the following complete example
for both a [text/template.Template] (import-aliased as `txtT.Template`) and
an [html/template.Template] (import-aliased as `htmlT.Template`).
import (
"fmt"
"log"
txtT "text/template"
htmlT "html/template"
`r00t2.io/goutils/tplx`
)
type (
S struct {
Name string
}
)
var (
tTpl *txtT.Template
hTpl *htmlT.Template
)
const tTplStr string = "Greetings, {{ .Name }}!\n"
const hTplStr string = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello, {{ .Name }}!</title>
</head>
<body>
<p>Hello, {{ .Name }}. Good to see you.</p>
</body>
</html>
`
func main() {
var err error
var s string
var o *S
o = &S{
Name: "Bob",
}
// A text template.
if tTpl, err = txtT.
New("my_txt_template").
Parse(tTplStr); err != nil {
log.Panicf("Failed to parse text template string '%s': %v\n", tTplStr, err)
}
if s, err = tplx.TplToStr[*txtT.Template](tTpl, o); err != nil {
log.Panicf("Failed to render text template to string: %v\n", err)
}
fmt.Println(s)
// An HTML template.
if hTpl, err = htmlT.
New("index.html").
Parse(hTplStr); err != nil {
log.Panicf("Failed to parse HTML template string '%s': %v\n", hTplStr, err)
}
if s, err = tplx.TplToStr[*htmlT.Template](hTpl, o); err != nil {
log.Panicf("Failed to render HTML template to string: %v\n", err)
}
fmt.Println(s)
}
Additionally, because this function uses a union type [Template],
you can even leave the type indicator off.
For example:
// ...
if s, err = tplx.TplToStr(tTpl, o); err != nil {
log.Panicf("Failed to render text template to string: %v\n", err)
}
// ...
if s, err = tplx.TplToStr(hTpl, o); err != nil {
log.Panicf("Failed to render HTML template to string: %v\n", err)
}
// ...
However, this is not recommended for readability purposes - including
the type indicator indicates (heh heh) to others reading your code
what type `tTpl` and `hTpl` are without needing to cross-reference
their declaration/assignment/definition.
For more information on generics in Golang, see:
* The introductory [blog post]
* The official [tutorial]
* The syntax [reference doc]
* The (community-maintained/unofficial) [Go by Example: Generics]
[blog post]: https://go.dev/blog/intro-generics
[tutorial]: https://go.dev/doc/tutorial/generics
[reference doc]: https://go.dev/ref/spec#Instantiations
[Go by Example: Generics]: https://gobyexample.com/generics
*/
func TplToStr[T Template](tpl T, obj any) (out string, err error) {
var buf *bytes.Buffer = new(bytes.Buffer)
if err = tpl.Execute(buf, obj); err != nil {
return
}
out = buf.String()
return
}
/*
TplToStrWith functions the exact same as [TplToStr] but allows you to specify the
template entry point (template name) named `nm`.
For example (see [TplToStr] for a full example):
// ...
var tplNm string = "index.html"
if s, err = tplx.TplToStrWith(tTpl, tplNm, o); err != nil {
log.Panicf("Failed to render HTML template '%s' to string: %v\n", tplNm, err)
}
// ...
would call the equivalent of:
// ...
if err = tpl.ExecuteTemplate(<internal buffer>, tplNm, o); err != nil {
// ...
}
*/
func TplToStrWith[T Template](tpl T, tplNm string, obj any) (out string, err error) {
var buf *bytes.Buffer = new(bytes.Buffer)
if err = tpl.ExecuteTemplate(buf, tplNm, obj); err != nil {
return
}
out = buf.String()
return
}

103
tplx/funcs_test.go Normal file
View File

@@ -0,0 +1,103 @@
package tplx
import (
htmlT `html/template`
`log`
"testing"
txtT `text/template`
)
const (
txtTplNm string = "my_txt_template"
htmlTplNm string = "index.html"
tgtTxt string = "Greetings, Bob!\n"
tgtHtml string = "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Hello, Bob!</title>\n\t</head>\n\t<body>\n\t\t<p>Hello, Bob. Good to see you.</p>\n\t</body>\n</html>\n"
tTplStr string = "Greetings, {{ .Name }}!\n"
hTplStr string = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello, {{ .Name }}!</title>
</head>
<body>
<p>Hello, {{ .Name }}. Good to see you.</p>
</body>
</html>
`
)
var (
tTpl *txtT.Template = txtT.Must(txtT.New(txtTplNm).Parse(tTplStr))
hTpl *htmlT.Template = htmlT.Must(htmlT.New(htmlTplNm).Parse(hTplStr))
o struct{ Name string } = struct{ Name string }{
Name: "Bob",
}
)
func TestTpl(t *testing.T) {
var err error
var s string
// if s, err = TplToStr[*txtT.Template](tTpl, o); err != nil {
if s, err = TplToStr(tTpl, o); err != nil {
t.Fatalf("Failed to render text template to string: %v\n", err)
}
t.Logf("Text template (%#v): '%s'", s, s)
if s != tgtTxt {
t.Fatalf("Mismatch on text template '%s'", s)
}
// if s, err = TplToStr[*htmlT.Template](hTpl, o); err != nil {
if s, err = TplToStr(hTpl, o); err != nil {
log.Panicf("Failed to render HTML template to string: %v\n", err)
}
t.Logf("HTML template (%#v):\n%s", s, s)
if s != tgtHtml {
t.Fatalf("Mismatch on HTML template '%s'", s)
}
}
func TestTplStr(t *testing.T) {
var err error
var s string
if s, err = TplStrToStr(tTplStr, TplTypeText, o); err != nil {
t.Fatalf("Failed to render text template to string: %v\n", err)
}
t.Logf("Text template (%#v): '%s'", s, s)
if s != tgtTxt {
t.Fatalf("Mismatch on text template '%s'", s)
}
if s, err = TplStrToStr(hTplStr, TplTypeHtml, o); err != nil {
log.Panicf("Failed to render HTML template to string: %v\n", err)
}
t.Logf("HTML template (%#v):\n%s", s, s)
if s != tgtHtml {
t.Fatalf("Mismatch on HTML template '%s'", s)
}
}
func TestTplWith(t *testing.T) {
var err error
var s string
if s, err = TplToStrWith(tTpl, txtTplNm, o); err != nil {
t.Fatalf("Failed to render text template to string: %v\n", err)
}
t.Logf("Text template (%#v): '%s'", s, s)
if s != tgtTxt {
t.Fatalf("Mismatch on text template '%s'", s)
}
if s, err = TplToStrWith(hTpl, htmlTplNm, o); err != nil {
log.Panicf("Failed to render HTML template to string: %v\n", err)
}
t.Logf("HTML template (%#v):\n%s", s, s)
if s != tgtHtml {
t.Fatalf("Mismatch on HTML template '%s'", s)
}
}

19
tplx/types.go Normal file
View File

@@ -0,0 +1,19 @@
package tplx
import (
htmlTpl `html/template`
`io`
txtTpl `text/template`
)
type (
tplType uint8
)
type (
Template interface {
*txtTpl.Template | *htmlTpl.Template
Execute(w io.Writer, obj any) (err error)
ExecuteTemplate(w io.Writer, tplNm string, obj any) (err error)
}
)