add MapMerge/MapMargeCopy functions for package gutil; improve template view feature for indefinite parameters

This commit is contained in:
John
2020-04-14 21:11:12 +08:00
parent 46ee070f0a
commit 0515fc94cb
10 changed files with 257 additions and 146 deletions

View File

@ -99,7 +99,7 @@ func (view *View) BindFuncMap(funcMap gview.FuncMap) {
// Display parses and writes the parsed template file content to http response.
func (view *View) Display(file ...string) error {
name := "index.tpl"
name := view.view.GetDefaultFile()
if len(file) > 0 {
name = file[0]
}

View File

@ -11,6 +11,7 @@ import (
"github.com/gogf/gf/os/gcfg"
"github.com/gogf/gf/os/gview"
"github.com/gogf/gf/util/gmode"
"github.com/gogf/gf/util/gutil"
)
// WriteTpl parses and responses given template file.
@ -74,27 +75,19 @@ func (r *Response) ParseTplContent(content string, params ...gview.Params) (stri
// buildInVars merges build-in variables into <params> and returns the new template variables.
func (r *Response) buildInVars(params ...map[string]interface{}) map[string]interface{} {
var vars map[string]interface{}
if len(params) > 0 && params[0] != nil {
vars = params[0]
} else {
vars = make(map[string]interface{})
}
m := gutil.MapMergeCopy(params...)
// Retrieve custom template variables from request object.
if len(r.Request.viewParams) > 0 {
for k, v := range r.Request.viewParams {
vars[k] = v
}
}
gutil.MapMerge(m, r.Request.viewParams, map[string]interface{}{
"Form": r.Request.GetFormMap(),
"Query": r.Request.GetQueryMap(),
"Request": r.Request.GetMap(),
"Cookie": r.Request.Cookie.Map(),
"Session": r.Request.Session.Map(),
})
// Note that it should assign no Config variable to template
// if there's no configuration file.
if c := gcfg.Instance(); c.Available() {
vars["Config"] = c.GetMap(".")
m["Config"] = c.GetMap(".")
}
vars["Form"] = r.Request.GetFormMap()
vars["Query"] = r.Request.GetQueryMap()
vars["Request"] = r.Request.GetMap()
vars["Cookie"] = r.Request.Cookie.Map()
vars["Session"] = r.Request.Session.Map()
return vars
return m
}

View File

@ -12,7 +12,6 @@ package gview
import (
"github.com/gogf/gf/container/gmap"
"github.com/gogf/gf/i18n/gi18n"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf"
@ -28,21 +27,12 @@ type View struct {
data map[string]interface{} // Global template variables.
funcMap map[string]interface{} // Global template function map.
fileCacheMap *gmap.StrAnyMap // File cache map.
defaultFile string // Default template file for parsing.
i18nManager *gi18n.Manager // I18n manager for this view.
delimiters []string // Custom template delimiters.
config Config // Extra configuration for the view.
}
// Params is type for template params.
type Params = map[string]interface{}
// FuncMap is type for custom template functions.
type FuncMap = map[string]interface{}
const (
// Default template file for parsing.
defaultParsingFile = "index.html"
type (
Params = map[string]interface{} // Params is type for template params.
FuncMap = map[string]interface{} // FuncMap is type for custom template functions.
)
var (
@ -60,9 +50,9 @@ func checkAndInitDefaultView() {
// ParseContent parses the template content directly using the default view object
// and returns the parsed content.
func ParseContent(content string, params Params) (string, error) {
func ParseContent(content string, params ...Params) (string, error) {
checkAndInitDefaultView()
return defaultViewObj.ParseContent(content, params)
return defaultViewObj.ParseContent(content, params...)
}
// New returns a new view object.
@ -73,9 +63,7 @@ func New(path ...string) *View {
data: make(map[string]interface{}),
funcMap: make(map[string]interface{}),
fileCacheMap: gmap.NewStrAnyMap(true),
defaultFile: defaultParsingFile,
i18nManager: gi18n.Instance(),
delimiters: make([]string, 2),
config: DefaultConfig(),
}
if len(path) > 0 && len(path[0]) > 0 {
if err := view.SetPath(path[0]); err != nil {
@ -118,35 +106,36 @@ func New(path ...string) *View {
"version": gf.VERSION,
}
// default build-in functions.
view.BindFunc("eq", view.funcEq)
view.BindFunc("ne", view.funcNe)
view.BindFunc("lt", view.funcLt)
view.BindFunc("le", view.funcLe)
view.BindFunc("gt", view.funcGt)
view.BindFunc("ge", view.funcGe)
view.BindFunc("text", view.funcText)
view.BindFuncMap(FuncMap{
"eq": view.funcEq,
"ne": view.funcNe,
"lt": view.funcLt,
"le": view.funcLe,
"gt": view.funcGt,
"ge": view.funcGe,
"text": view.funcText,
"html": view.funcHtmlEncode,
"htmlencode": view.funcHtmlEncode,
"htmldecode": view.funcHtmlDecode,
"encode": view.funcHtmlEncode,
"decode": view.funcHtmlDecode,
"url": view.funcUrlEncode,
"urlencode": view.funcUrlEncode,
"urldecode": view.funcUrlDecode,
"date": view.funcDate,
"substr": view.funcSubStr,
"strlimit": view.funcStrLimit,
"concat": view.funcConcat,
"replace": view.funcReplace,
"compare": view.funcCompare,
"hidestr": view.funcHideStr,
"highlight": view.funcHighlight,
"toupper": view.funcToUpper,
"tolower": view.funcToLower,
"nl2br": view.funcNl2Br,
"include": view.funcInclude,
"dump": view.funcDump,
})
view.BindFunc("html", view.funcHtmlEncode)
view.BindFunc("htmlencode", view.funcHtmlEncode)
view.BindFunc("htmldecode", view.funcHtmlDecode)
view.BindFunc("encode", view.funcHtmlEncode)
view.BindFunc("decode", view.funcHtmlDecode)
view.BindFunc("url", view.funcUrlEncode)
view.BindFunc("urlencode", view.funcUrlEncode)
view.BindFunc("urldecode", view.funcUrlDecode)
view.BindFunc("date", view.funcDate)
view.BindFunc("substr", view.funcSubStr)
view.BindFunc("strlimit", view.funcStrLimit)
view.BindFunc("concat", view.funcConcat)
view.BindFunc("replace", view.funcReplace)
view.BindFunc("compare", view.funcCompare)
view.BindFunc("hidestr", view.funcHideStr)
view.BindFunc("highlight", view.funcHighlight)
view.BindFunc("toupper", view.funcToUpper)
view.BindFunc("tolower", view.funcToLower)
view.BindFunc("nl2br", view.funcNl2Br)
view.BindFunc("include", view.funcInclude)
view.BindFunc("dump", view.funcDump)
return view
}

View File

@ -22,10 +22,25 @@ import (
// Config is the configuration object for template engine.
type Config struct {
Paths []string // Searching array for path, NOT concurrent-safe for performance purpose.
Data map[string]interface{} // Global template variables.
Data map[string]interface{} // Global template variables including configuration.
DefaultFile string // Default template file for parsing.
Delimiters []string // Custom template delimiters.
AutoEncode bool // Automatically encodes and provides safe html output, which is good for avoiding XSS.
I18nManager *gi18n.Manager // I18n manager for the view.
}
const (
// Default template file for parsing.
defaultParsingFile = "index.html"
)
// DefaultConfig creates and returns a configuration object with default configurations.
func DefaultConfig() Config {
return Config{
DefaultFile: defaultParsingFile,
I18nManager: gi18n.Instance(),
Delimiters: make([]string, 2),
}
}
// SetConfig sets the configuration for view.
@ -199,13 +214,17 @@ func (view *View) Assign(key string, value interface{}) {
// SetDefaultFile sets default template file for parsing.
func (view *View) SetDefaultFile(file string) {
view.defaultFile = file
view.config.DefaultFile = file
}
// GetDefaultFile returns default template file for parsing.
func (view *View) GetDefaultFile() string {
return view.config.DefaultFile
}
// SetDelimiters sets customized delimiters for template parsing.
func (view *View) SetDelimiters(left, right string) {
view.delimiters[0] = left
view.delimiters[1] = right
view.config.Delimiters = []string{left, right}
}
// SetAutoEncode enables/disables automatically html encoding feature.
@ -237,5 +256,5 @@ func (view *View) BindFuncMap(funcMap FuncMap) {
// SetI18n binds i18n manager to current view engine.
func (view *View) SetI18n(manager *gi18n.Manager) {
view.i18nManager = manager
view.config.I18nManager = manager
}

View File

@ -10,14 +10,14 @@ import "github.com/gogf/gf/util/gconv"
// i18nTranslate translate the content with i18n feature.
func (view *View) i18nTranslate(content string, params Params) string {
if view.i18nManager != nil {
if view.config.I18nManager != nil {
if v, ok := params["I18nLanguage"]; ok {
language := gconv.String(v)
if language != "" {
return view.i18nManager.T(content, language)
return view.config.I18nManager.T(content, language)
}
}
return view.i18nManager.T(content)
return view.config.I18nManager.T(content)
}
return content
}

View File

@ -17,6 +17,7 @@ import (
"github.com/gogf/gf/os/gmlock"
"github.com/gogf/gf/text/gstr"
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gutil"
htmltpl "html/template"
"strconv"
"strings"
@ -113,32 +114,9 @@ func (view *View) Parse(file string, params ...Params) (result string, err error
// Note that the template variable assignment cannot change the value
// of the existing <params> or view.data because both variables are pointers.
// It needs to merge the values of the two maps into a new map.
var variables map[string]interface{}
length := len(view.data)
if len(params) > 0 {
length += len(params[0])
}
if length > 0 {
variables = make(map[string]interface{}, length)
}
variables := gutil.MapMergeCopy(params...)
if len(view.data) > 0 {
if len(params) > 0 {
if variables == nil {
variables = make(map[string]interface{})
}
for k, v := range params[0] {
variables[k] = v
}
for k, v := range view.data {
variables[k] = v
}
} else {
variables = view.data
}
} else {
if len(params) > 0 {
variables = params[0]
}
gutil.MapMerge(variables, view.data)
}
buffer := bytes.NewBuffer(nil)
if view.config.AutoEncode {
@ -163,7 +141,7 @@ func (view *View) Parse(file string, params ...Params) (result string, err error
// ParseDefault parses the default template file with params.
func (view *View) ParseDefault(params ...Params) (result string, err error) {
return view.Parse(view.defaultFile, params...)
return view.Parse(view.config.DefaultFile, params...)
}
// ParseContent parses given template content <content> with template variables <params>
@ -174,12 +152,18 @@ func (view *View) ParseContent(content string, params ...Params) (string, error)
return "", nil
}
err := (error)(nil)
key := fmt.Sprintf("%s_%v_%v", gCONTENT_TEMPLATE_NAME, view.delimiters, view.config.AutoEncode)
key := fmt.Sprintf("%s_%v_%v", gCONTENT_TEMPLATE_NAME, view.config.Delimiters, view.config.AutoEncode)
tpl := templates.GetOrSetFuncLock(key, func() interface{} {
if view.config.AutoEncode {
return htmltpl.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
return htmltpl.New(gCONTENT_TEMPLATE_NAME).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
}
return texttpl.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
return texttpl.New(gCONTENT_TEMPLATE_NAME).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
})
// Using memory lock to ensure concurrent safety for content parsing.
hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10)
@ -196,32 +180,9 @@ func (view *View) ParseContent(content string, params ...Params) (string, error)
// Note that the template variable assignment cannot change the value
// of the existing <params> or view.data because both variables are pointers.
// It needs to merge the values of the two maps into a new map.
var variables map[string]interface{}
length := len(view.data)
if len(params) > 0 {
length += len(params[0])
}
if length > 0 {
variables = make(map[string]interface{}, length)
}
variables := gutil.MapMergeCopy(params...)
if len(view.data) > 0 {
if len(params) > 0 {
if variables == nil {
variables = make(map[string]interface{})
}
for k, v := range params[0] {
variables[k] = v
}
for k, v := range view.data {
variables[k] = v
}
} else {
variables = view.data
}
} else {
if len(params) > 0 {
variables = params[0]
}
gutil.MapMerge(variables, view.data)
}
buffer := bytes.NewBuffer(nil)
if view.config.AutoEncode {
@ -249,14 +210,20 @@ func (view *View) ParseContent(content string, params ...Params) (string, error)
// if the template files under <path> changes (recursively).
func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) {
// Key for template cache.
key := fmt.Sprintf("%s_%v", filePath, view.delimiters)
key := fmt.Sprintf("%s_%v", filePath, view.config.Delimiters)
result := templates.GetOrSetFuncLock(key, func() interface{} {
// Do not use <key> but the <filePath> as the parameter <name> for function New,
// because when error occurs the <name> will be printed out for error locating.
if view.config.AutoEncode {
tpl = htmltpl.New(filePath).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
tpl = htmltpl.New(filePath).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
} else {
tpl = texttpl.New(filePath).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
tpl = texttpl.New(filePath).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
}
// Firstly checking the resource manager.
if !gres.IsEmpty() {

View File

@ -8,12 +8,6 @@ package gutil
import (
"github.com/gogf/gf/internal/utils"
"regexp"
)
var (
// replaceCharReg is the regular expression object for replacing chars in map keys.
replaceCharReg, _ = regexp.Compile(`[\-\.\_\s]+`)
)
// MapCopy does a shallow copy from map <data> to <copy> for most commonly used map type
@ -32,6 +26,29 @@ func MapContains(data map[string]interface{}, key string) (ok bool) {
return
}
// MapMerge merges all map from <src> to map <dst>.
func MapMerge(dst map[string]interface{}, src ...map[string]interface{}) {
if dst == nil {
return
}
for _, m := range src {
for k, v := range m {
dst[k] = v
}
}
}
// MapMergeCopy creates and returns a new map which merges all map from <src>.
func MapMergeCopy(src ...map[string]interface{}) (copy map[string]interface{}) {
copy = make(map[string]interface{})
for _, m := range src {
for k, v := range m {
copy[k] = v
}
}
return
}
// MapPossibleItemByKey tries to find the possible key-value pair for given key with or without
// cases or chars '-'/'_'/'.'/' '.
//

View File

@ -12,6 +12,15 @@ import (
"testing"
)
var (
m1 = map[string]interface{}{
"k1": "v1",
}
m2 = map[string]interface{}{
"k2": "v2",
}
)
func Benchmark_TryCatch(b *testing.B) {
for i := 0; i < b.N; i++ {
TryCatch(func() {
@ -21,3 +30,9 @@ func Benchmark_TryCatch(b *testing.B) {
})
}
}
func Benchmark_MapMergeCopy(b *testing.B) {
for i := 0; i < b.N; i++ {
MapMergeCopy(m1, m2)
}
}

View File

@ -0,0 +1,120 @@
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gutil_test
import (
"github.com/gogf/gf/frame/g"
"testing"
"github.com/gogf/gf/test/gtest"
"github.com/gogf/gf/util/gutil"
)
func Test_MapCopy(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m1 := g.Map{
"k1": "v1",
}
m2 := gutil.MapCopy(m1)
m2["k2"] = "v2"
t.Assert(m1["k1"], "v1")
t.Assert(m1["k2"], nil)
t.Assert(m2["k1"], "v1")
t.Assert(m2["k2"], "v2")
})
}
func Test_MapContains(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m1 := g.Map{
"k1": "v1",
}
t.Assert(gutil.MapContains(m1, "k1"), true)
t.Assert(gutil.MapContains(m1, "K1"), false)
t.Assert(gutil.MapContains(m1, "k2"), false)
})
}
func Test_MapMerge(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m1 := g.Map{
"k1": "v1",
}
m2 := g.Map{
"k2": "v2",
}
m3 := g.Map{
"k3": "v3",
}
gutil.MapMerge(m1, m2, m3, nil)
t.Assert(m1["k1"], "v1")
t.Assert(m1["k2"], "v2")
t.Assert(m1["k3"], "v3")
t.Assert(m2["k1"], nil)
t.Assert(m3["k1"], nil)
})
}
func Test_MapMergeCopy(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m1 := g.Map{
"k1": "v1",
}
m2 := g.Map{
"k2": "v2",
}
m3 := g.Map{
"k3": "v3",
}
m := gutil.MapMergeCopy(m1, m2, m3, nil)
t.Assert(m["k1"], "v1")
t.Assert(m["k2"], "v2")
t.Assert(m["k3"], "v3")
t.Assert(m1["k1"], "v1")
t.Assert(m1["k2"], nil)
t.Assert(m2["k1"], nil)
t.Assert(m3["k1"], nil)
})
}
func Test_MapPossibleItemByKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := g.Map{
"name": "guo",
"NickName": "john",
}
k, v := gutil.MapPossibleItemByKey(m, "NAME")
t.Assert(k, "name")
t.Assert(v, "guo")
k, v = gutil.MapPossibleItemByKey(m, "nick name")
t.Assert(k, "NickName")
t.Assert(v, "john")
k, v = gutil.MapPossibleItemByKey(m, "none")
t.Assert(k, "")
t.Assert(v, nil)
})
}
func Test_MapContainsPossibleKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := g.Map{
"name": "guo",
"NickName": "john",
}
t.Assert(gutil.MapContainsPossibleKey(m, "name"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "NAME"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "nickname"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "nick name"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "nick_name"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "nick-name"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "nick.name"), true)
t.Assert(gutil.MapContainsPossibleKey(m, "none"), false)
})
}

View File

@ -19,18 +19,9 @@ func Test_Dump(t *testing.T) {
100: 100,
})
})
gtest.C(t, func(t *gtest.T) {
gutil.Dump(map[string]interface{}{"": func() {}})
})
gtest.C(t, func(t *gtest.T) {
gutil.Dump([]byte("gutil Dump test"))
})
}
func Test_TryCatch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
gutil.TryCatch(func() {
panic("gutil TryCatch test")