Skip to content

Commit

Permalink
✨ up: textutil - rename the StrTemplate to LiteTemplate, and update s…
Browse files Browse the repository at this point in the history
…ome render logic
  • Loading branch information
inhere committed Sep 8, 2023
1 parent 8f8f7ee commit b6adb71
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 54 deletions.
104 changes: 61 additions & 43 deletions strutil/textutil/strtpl.go → strutil/textutil/litetpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (
"github.com/gookit/goutil/strutil"
)

// STemplateOptFn template option func
type STemplateOptFn func(opt *StrTemplateOpt)
// LTemplateOptFn lite template option func
type LTemplateOptFn func(opt *LiteTemplateOpt)

// StrTemplateOpt template options for StrTemplate
type StrTemplateOpt struct {
// LiteTemplateOpt template options for LiteTemplate
type LiteTemplateOpt struct {
// func name alias map. eg: {"up_first": "upFirst"}
nameMp structs.Aliases
Funcs template.FuncMap
Expand All @@ -30,7 +30,7 @@ type StrTemplateOpt struct {
ParseEnv bool
}

// StrTemplate implement a simple string template
// LiteTemplate implement a simple text template engine.
//
// - support parse template vars
// - support access multi-level map field. eg: {{ user.name }}
Expand All @@ -39,52 +39,46 @@ type StrTemplateOpt struct {
// - support custom pipeline func handle. eg: {{ name | upper }} {{ name | def:guest }}
//
// NOTE: not support control flow, eg: if/else/for/with
type StrTemplate struct {
StrTemplateOpt
type LiteTemplate struct {
LiteTemplateOpt
vr VarReplacer
// template func map. refer the text/template
//
// Func allow return 1 or 2 values, if return 2 values, the second value is error.
fxs map[string]*reflects.FuncX
}

// NewStrTemplate instance
func NewStrTemplate(opFns ...STemplateOptFn) *StrTemplate {
st := &StrTemplate{
// NewLiteTemplate instance
func NewLiteTemplate(opFns ...LTemplateOptFn) *LiteTemplate {
st := &LiteTemplate{
fxs: make(map[string]*reflects.FuncX),
vr: VarReplacer{
Left: "{{",
Right: "}}",
// with default options
LiteTemplateOpt: LiteTemplateOpt{
Left: "{{",
Right: "}}",
ParseDef: true,
ParseEnv: true,
},
}

st.ParseDef = true
st.ParseEnv = true
st.vr.RenderFn = st.renderVars
for _, fn := range opFns {
fn(&st.StrTemplateOpt)
fn(&st.LiteTemplateOpt)
}

st.Init()
return st
}

// Init StrTemplate
func (t *StrTemplate) Init() {
// Init LiteTemplate
func (t *LiteTemplate) Init() {
if t.vr.init {
return
}

basefn.PanicIf(t.vr.Right == "", "var format Right chars is required")

// init var replacer
t.vr.init = true
t.vr.parseDef = t.ParseDef
t.vr.parseEnv = t.ParseEnv

t.vr.lLen, t.vr.rLen = len(t.vr.Left), len(t.vr.Right)
// (?s:...) - 让 "." 匹配换行
// (?s:(.+?)) - 第二个 "?" 非贪婪匹配
t.vr.varReg = regexp.MustCompile(regexp.QuoteMeta(t.vr.Left) + `(?s:(.+?))` + regexp.QuoteMeta(t.vr.Right))
t.initReplacer(&t.vr)

// add built-in funcs
t.AddFuncs(builtInFuncs)
Expand All @@ -93,26 +87,40 @@ func (t *StrTemplate) Init() {
"lc_first": "lcFirst",
"def": "default",
})

// add custom funcs
if len(t.Funcs) > 0 {
t.AddFuncs(t.Funcs)
}
}

func (t *LiteTemplate) initReplacer(vr *VarReplacer) {
vr.flatSubs = true
vr.parseDef = t.ParseDef
vr.parseEnv = t.ParseEnv
vr.Left, vr.Right = t.Left, t.Right
basefn.PanicIf(vr.Right == "", "var format right chars is required")

vr.lLen, vr.rLen = len(vr.Left), len(vr.Right)
// (?s:...) - 让 "." 匹配换行
// (?s:(.+?)) - 第二个 "?" 非贪婪匹配
vr.varReg = regexp.MustCompile(regexp.QuoteMeta(vr.Left) + `(?s:(.+?))` + regexp.QuoteMeta(vr.Right))
}

// AddFuncs add custom template functions
func (t *StrTemplate) AddFuncs(fns map[string]any) {
func (t *LiteTemplate) AddFuncs(fns map[string]any) {
for name, fn := range fns {
t.fxs[name] = reflects.NewFunc(fn)
}
}

// RenderString render template string with vars
func (t *StrTemplate) RenderString(s string, vars map[string]any) string {
func (t *LiteTemplate) RenderString(s string, vars map[string]any) string {
return t.vr.Replace(s, vars)
}

// RenderFile render template file with vars
func (t *StrTemplate) RenderFile(filePath string, vars map[string]any) (string, error) {
if !fsutil.FileExists(filePath) {
return "", fmt.Errorf("template file not exists: %s", filePath)
}

func (t *LiteTemplate) RenderFile(filePath string, vars map[string]any) (string, error) {
// read file contents
s, err := fsutil.ReadStringOrErr(filePath)
if err != nil {
Expand All @@ -122,18 +130,18 @@ func (t *StrTemplate) RenderFile(filePath string, vars map[string]any) (string,
return t.vr.Replace(s, vars), nil
}

// RenderWrite render template string with vars, and write to writer
func (t *StrTemplate) RenderWrite(wr io.Writer, s string, vars map[string]any) error {
// RenderWrite render template string with vars, and write result to writer
func (t *LiteTemplate) RenderWrite(wr io.Writer, s string, vars map[string]any) error {
s = t.vr.Replace(s, vars)
_, err := io.WriteString(wr, s)
return err
}

func (t *StrTemplate) renderVars(s string, varMap map[string]string) string {
func (t *LiteTemplate) renderVars(s string, varMap map[string]string) string {
return t.vr.varReg.ReplaceAllStringFunc(s, func(sub string) string {
// var name or pipe expression.
name := strings.TrimSpace(sub[t.vr.lLen : len(sub)-t.vr.rLen])
name = strings.TrimLeft(name, ".")
name = strings.TrimLeft(name, "$.")

var defVal string
var pipes []string
Expand Down Expand Up @@ -183,7 +191,7 @@ func (t *StrTemplate) renderVars(s string, varMap map[string]string) string {
})
}

func (t *StrTemplate) applyPipes(val any, pipes []string) (string, error) {
func (t *LiteTemplate) applyPipes(val any, pipes []string) (string, error) {
var err error

// pipe expr: "trim|upper|substr:1,2"
Expand Down Expand Up @@ -218,7 +226,7 @@ func (t *StrTemplate) applyPipes(val any, pipes []string) (string, error) {
return strutil.ToString(val)
}

func (t *StrTemplate) isFunc(name string) bool {
func (t *LiteTemplate) isFunc(name string) bool {
_, ok := t.fxs[name]
if !ok {
// check name alias
Expand All @@ -227,17 +235,27 @@ func (t *StrTemplate) isFunc(name string) bool {
return ok
}

func (t *StrTemplate) isDefaultFunc(name string) bool {
func (t *LiteTemplate) isDefaultFunc(name string) bool {
return name == "default" || name == "def"
}

var stdTpl = NewStrTemplate()
var stdTpl = NewLiteTemplate()

// RenderFile render template file with vars
func RenderFile(filePath string, vars map[string]any) (string, error) {
return stdTpl.RenderFile(filePath, vars)
}

// RenderString render str template string or file.
func RenderString(input string, data map[string]any, optFns ...RenderOptFn) string {
func RenderString(input string, data map[string]any) string {
return stdTpl.RenderString(input, data)
}

// RenderWrite render template string with vars, and write result to writer
func RenderWrite(wr io.Writer, s string, vars map[string]any) error {
return stdTpl.RenderWrite(wr, s, vars)
}

func parseArgStr(argStr string) (ss []any) {
if argStr == "" { // no arg
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package textutil_test

import (
"fmt"
"testing"

"github.com/gookit/goutil/strutil/textutil"
"github.com/gookit/goutil/testutil/assert"
)

func TestRenderString(t *testing.T) {
data := map[string]any{
"name": "inhere",
"age": 2000,
}
var data = map[string]any{
"name": "inhere",
"age": 2000,
"subMp": map[string]any{
"city": "cd",
"addr": "addr 001",
},
}

func TestRenderString(t *testing.T) {
t.Run("basic", func(t *testing.T) {
tpl := "hi, My name is {{ .name | upFirst }}, age is {{ .age }}"
str := textutil.RenderString(tpl, data)
Expand All @@ -26,3 +31,17 @@ func TestRenderString(t *testing.T) {
assert.Eq(t, "name: guest, age: 18, city: cd", str)
})
}

func TestRenderFile(t *testing.T) {
s, err := textutil.RenderFile("testdata/test-lite.tpl", data)
assert.NoError(t, err)
fmt.Println(s)

assert.StrContains(t, s, "hi, My name is Inhere, age is 2000")
assert.StrContains(t, s, "City: CD")
assert.StrContains(t, s, "Addr: addr 001")

// file not exist
_, err = textutil.RenderFile("testdata/not-exist.tpl", nil)
assert.Error(t, err)
}
4 changes: 4 additions & 0 deletions strutil/textutil/testdata/test-lite.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
hi, My name is {{ .name | upFirst }}, age is {{ .age }}

- City: {{ $subMp.city | upper }}
- Addr: {{ $subMp.addr }}
File renamed without changes.
12 changes: 6 additions & 6 deletions strutil/textutil/var_replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,13 @@ func (r *VarReplacer) ParseVars(s string) []string {
return arrutil.Unique(ss)
}

// Render any-map vars in the text contents
func (r *VarReplacer) Render(s string, tplVars map[string]any) string {
return r.Replace(s, tplVars)
}

// Replace any-map vars in the text contents
func (r *VarReplacer) Replace(s string, tplVars map[string]any) string {
return r.Render(s, tplVars)
}

// Render any-map vars in the text contents
func (r *VarReplacer) Render(s string, tplVars map[string]any) string {
if !strings.Contains(s, r.Left) {
return s
}
Expand All @@ -139,8 +139,8 @@ func (r *VarReplacer) Replace(s string, tplVars map[string]any) string {
}

r.Init()
var varMap map[string]string

var varMap map[string]string
if r.flatSubs {
varMap = make(map[string]string, len(tplVars)*2)
maputil.FlatWithFunc(tplVars, func(path string, val reflect.Value) {
Expand Down

0 comments on commit b6adb71

Please sign in to comment.