Initial QSfera import
This commit is contained in:
+277
@@ -0,0 +1,277 @@
|
||||
package textutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/fsutil"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/structs"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
// LTemplateOptFn lite template option func
|
||||
type LTemplateOptFn func(opt *LiteTemplateOpt)
|
||||
|
||||
// LiteTemplateOpt template options for LiteTemplate
|
||||
type LiteTemplateOpt struct {
|
||||
// func name alias map. eg: {"up_first": "upFirst"}
|
||||
nameMp structs.Aliases
|
||||
Funcs template.FuncMap
|
||||
|
||||
Left, Right string
|
||||
|
||||
ParseDef bool
|
||||
ParseEnv bool
|
||||
}
|
||||
|
||||
// SetVarFmt custom sets the variable format in template
|
||||
func (o *LiteTemplateOpt) SetVarFmt(varFmt string) {
|
||||
o.Left, o.Right = strutil.TrimCut(varFmt, ",")
|
||||
}
|
||||
|
||||
// LiteTemplate implement a simple text template engine.
|
||||
//
|
||||
// - support parse template vars
|
||||
// - support access multi-level map field. eg: {{ user.name }}
|
||||
// - support parse default value
|
||||
// - support parse env vars
|
||||
// - support custom pipeline func handle. eg: {{ name | upper }} {{ name | def:guest }}
|
||||
//
|
||||
// NOTE: not support control flow, eg: if/else/for/with
|
||||
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
|
||||
}
|
||||
|
||||
// NewLiteTemplate instance
|
||||
func NewLiteTemplate(opFns ...LTemplateOptFn) *LiteTemplate {
|
||||
st := &LiteTemplate{
|
||||
fxs: make(map[string]*reflects.FuncX),
|
||||
// with default options
|
||||
LiteTemplateOpt: LiteTemplateOpt{
|
||||
Left: "{{",
|
||||
Right: "}}",
|
||||
ParseDef: true,
|
||||
ParseEnv: true,
|
||||
},
|
||||
}
|
||||
|
||||
st.vr.RenderFn = st.renderVars
|
||||
for _, fn := range opFns {
|
||||
fn(&st.LiteTemplateOpt)
|
||||
}
|
||||
|
||||
st.Init()
|
||||
return st
|
||||
}
|
||||
|
||||
// Init LiteTemplate
|
||||
func (t *LiteTemplate) Init() {
|
||||
if t.vr.init {
|
||||
return
|
||||
}
|
||||
|
||||
// init var replacer
|
||||
t.vr.init = true
|
||||
t.initReplacer(&t.vr)
|
||||
|
||||
// add built-in funcs
|
||||
t.AddFuncs(builtInFuncs)
|
||||
t.nameMp.AddAliasMap(map[string]string{
|
||||
"up_first": "upFirst",
|
||||
"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)
|
||||
rightLast := string(vr.Right[vr.rLen-1]) // 排除匹配,防止匹配到类似 "{} adb ddf {var}"
|
||||
|
||||
// eg: \{(?s:([^\}]+?))\}
|
||||
// (?s:...) - 让 "." 匹配换行
|
||||
// (?s:(.+?)) - 第二个 "?" 非贪婪匹配
|
||||
pattern := regexp.QuoteMeta(vr.Left) + `(?s:([^` + regexp.QuoteMeta(rightLast) + `]+?))` + regexp.QuoteMeta(vr.Right)
|
||||
vr.varReg = regexp.MustCompile(pattern)
|
||||
}
|
||||
|
||||
// AddFuncs add custom template functions
|
||||
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 *LiteTemplate) RenderString(s string, vars map[string]any) string {
|
||||
return t.vr.Replace(s, vars)
|
||||
}
|
||||
|
||||
// RenderFile render template file with vars
|
||||
func (t *LiteTemplate) RenderFile(filePath string, vars map[string]any) (string, error) {
|
||||
// read file contents
|
||||
s, err := fsutil.ReadStringOrErr(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return t.vr.Replace(s, vars), nil
|
||||
}
|
||||
|
||||
// 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 *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, "$.")
|
||||
|
||||
var defVal string
|
||||
var pipes []string
|
||||
if strings.ContainsRune(name, '|') {
|
||||
pipes = strutil.Split(name, "|")
|
||||
// compatible default value. eg: {{ name | inhere }}
|
||||
if len(pipes) == 2 && !strings.ContainsRune(pipes[1], ':') && !t.isFunc(pipes[1]) {
|
||||
name, defVal = pipes[0], pipes[1]
|
||||
pipes = nil // clear pipes
|
||||
} else { // collect pipe functions
|
||||
name, pipes = pipes[0], pipes[1:]
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := varMap[name]; ok {
|
||||
if len(pipes) > 0 {
|
||||
var err error
|
||||
val, err = t.applyPipes(val, pipes)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Render var %q error: %v", name, err)
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// var not found
|
||||
if len(defVal) > 0 {
|
||||
return defVal
|
||||
}
|
||||
|
||||
if t.vr.NotFound != nil {
|
||||
if val, ok := t.vr.NotFound(name); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// check is default func. eg: {{ name | def:guest }}
|
||||
if len(pipes) == 1 && strings.ContainsRune(pipes[0], ':') {
|
||||
fName, argVal := strutil.TrimCut(pipes[0], ":")
|
||||
if t.isDefaultFunc(fName) {
|
||||
return argVal
|
||||
}
|
||||
}
|
||||
|
||||
t.vr.missVars = append(t.vr.missVars, name)
|
||||
return sub
|
||||
})
|
||||
}
|
||||
|
||||
func (t *LiteTemplate) applyPipes(val any, pipes []string) (string, error) {
|
||||
var err error
|
||||
|
||||
// pipe expr: "trim|upper|substr:1,2"
|
||||
// =>
|
||||
// pipes: ["trim", "upper", "substr:1,2"]
|
||||
for _, name := range pipes {
|
||||
args := []any{val}
|
||||
|
||||
// has custom args. eg: "substr:1,2"
|
||||
if strings.ContainsRune(name, ':') {
|
||||
var argStr string
|
||||
name, argStr = strutil.TrimCut(name, ":")
|
||||
|
||||
if otherArgs := parseArgStr(argStr); len(otherArgs) > 0 {
|
||||
args = append(args, otherArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
name = t.nameMp.ResolveAlias(name)
|
||||
|
||||
// call pipe func
|
||||
if fx, ok := t.fxs[name]; ok {
|
||||
val, err = fx.Call2(args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("template func %q not found", name)
|
||||
}
|
||||
}
|
||||
|
||||
return strutil.ToString(val)
|
||||
}
|
||||
|
||||
func (t *LiteTemplate) isFunc(name string) bool {
|
||||
_, ok := t.fxs[name]
|
||||
if !ok {
|
||||
// check name alias
|
||||
return t.nameMp.HasAlias(name)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (t *LiteTemplate) isDefaultFunc(name string) bool {
|
||||
return name == "default" || name == "def"
|
||||
}
|
||||
|
||||
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) 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
|
||||
}
|
||||
|
||||
if len(argStr) == 1 { // one char
|
||||
return []any{argStr}
|
||||
}
|
||||
return arrutil.StringsToAnys(strutil.Split(argStr, ","))
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package textutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/gookit/goutil/reflects"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// type SimpleAnyFunc func(args ...any) any
|
||||
|
||||
// StrVarRenderer implements like shell vars renderer
|
||||
// 简单的实现类似 php, kotlin, shell 插值变量渲染,表达式解析处理。
|
||||
//
|
||||
// - var format: $var_name, ${some_var}, ${top.sub_var}
|
||||
// - func call: ${func($var_name, 'const string')}
|
||||
type StrVarRenderer struct {
|
||||
// global variables
|
||||
vars map[string]any
|
||||
// fallback func for var not exists
|
||||
getter FallbackFn
|
||||
// funcMap map[string]any TODO use any, add reflect value to rfs
|
||||
funcMap map[string]func(...any) any
|
||||
// var func map. refer the text/template TODO
|
||||
//
|
||||
// Func allow return 1 or 2 values, if return 2 values, the second value is error.
|
||||
rfs map[string]*reflects.FuncX
|
||||
}
|
||||
|
||||
// NewStrVarRenderer create a new StrVarRenderer
|
||||
func NewStrVarRenderer() *StrVarRenderer {
|
||||
return &StrVarRenderer{
|
||||
vars: make(map[string]any),
|
||||
// funcMap: make(map[string]any),
|
||||
funcMap: make(map[string]func(...any) any),
|
||||
}
|
||||
}
|
||||
|
||||
// SetVars set variables
|
||||
func (r *StrVarRenderer) SetVars(vars map[string]any) *StrVarRenderer {
|
||||
for k, v := range vars {
|
||||
r.vars[k] = v
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetVar set a variable
|
||||
func (r *StrVarRenderer) SetVar(name string, value any) *StrVarRenderer {
|
||||
r.vars[name] = value
|
||||
return r
|
||||
}
|
||||
|
||||
// SetFuncMap set function map
|
||||
func (r *StrVarRenderer) SetFuncMap(funcMap map[string]func(...any) any) *StrVarRenderer {
|
||||
for k, v := range funcMap {
|
||||
r.funcMap[k] = v
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetFunc set a function
|
||||
func (r *StrVarRenderer) SetFunc(name string, fn func(...any) any) *StrVarRenderer {
|
||||
r.funcMap[name] = fn
|
||||
return r
|
||||
}
|
||||
|
||||
// SetGetter set variable getter
|
||||
func (r *StrVarRenderer) SetGetter(getter FallbackFn) *StrVarRenderer {
|
||||
r.getter = getter
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
// 处理 $var_name 格式
|
||||
// - 允许:$1..$N 这样的变量
|
||||
// - 也支持 $@, $* 变量
|
||||
reS = regexp.MustCompile(`\$(\w[a-zA-Z0-9_]*|[@|*])`)
|
||||
// 处理 ${var_name} ${top.sub} 格式
|
||||
reQ = regexp.MustCompile(`\$\{([a-zA-Z][a-zA-Z0-9_.]*)\}`)
|
||||
// 处理 ${func(...)} 格式
|
||||
reFn = regexp.MustCompile(`\$\{([a-zA-Z][a-zA-Z0-9_]*)\(([^}]*)\)\}`)
|
||||
)
|
||||
|
||||
// Render rendering input string with variables
|
||||
func (r *StrVarRenderer) Render(input string, vars map[string]any) string {
|
||||
vars = maputil.Merge1level(r.vars, vars)
|
||||
data := maputil.Map(vars)
|
||||
|
||||
// 处理 $var_name 格式
|
||||
input = r.replaceVars(input, reS, data)
|
||||
|
||||
if strings.Contains(input, "${") {
|
||||
// 处理 ${var.name} 格式
|
||||
input = r.replaceVars(input, reQ, data)
|
||||
// 处理 ${func(...)} 格式
|
||||
input = r.handleFuncCalls(input, reFn, data)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func (r *StrVarRenderer) handleFuncCalls(input string, re *regexp.Regexp, vars maputil.Map) string {
|
||||
return re.ReplaceAllStringFunc(input, func(matched string) string {
|
||||
submatch := re.FindStringSubmatch(matched)
|
||||
funcName := submatch[1]
|
||||
|
||||
// 解析参数 并 调用函数
|
||||
if fn, ok := r.funcMap[funcName]; ok {
|
||||
args := r.parseArgs(submatch[2], vars)
|
||||
result := fn(args...)
|
||||
return fmt.Sprint(result)
|
||||
}
|
||||
|
||||
return matched
|
||||
})
|
||||
}
|
||||
|
||||
func (r *StrVarRenderer) parseArgs(argsStr string, data maputil.Map) []any {
|
||||
if argsStr == "" {
|
||||
return []any{}
|
||||
}
|
||||
|
||||
// 简单参数解析,按逗号分割
|
||||
args := strings.Split(argsStr, ",")
|
||||
results := make([]any, len(args))
|
||||
|
||||
for i, arg := range args {
|
||||
arg = strings.TrimSpace(arg)
|
||||
// is var name
|
||||
if arg[0] == '$' {
|
||||
results[i] = data.Get(arg[1:])
|
||||
continue
|
||||
}
|
||||
|
||||
// 常量值:去掉引号
|
||||
last := len(arg) - 1
|
||||
// strconv.Unquote( arg)
|
||||
if (arg[0] == '"' && arg[last] == '"') || (arg[0] == '\'' && arg[last] == '\'') {
|
||||
results[i] = arg[1 : len(arg)-1]
|
||||
} else if arg == "true" || arg == "false" {
|
||||
results[i] = strutil.SafeBool(arg)
|
||||
} else if strutil.IsInt(arg) {
|
||||
results[i] = strutil.SafeInt(arg)
|
||||
} else {
|
||||
results[i] = arg
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *StrVarRenderer) replaceVars(input string, re *regexp.Regexp, data maputil.Map) string {
|
||||
return re.ReplaceAllStringFunc(input, func(matched string) string {
|
||||
var varName string
|
||||
|
||||
// format: ${var_name} 提取变量名
|
||||
if strings.HasPrefix(matched, "${") {
|
||||
varName = matched[2 : len(matched)-1]
|
||||
} else {
|
||||
// format: $var_name
|
||||
varName = matched[1:]
|
||||
}
|
||||
|
||||
// 从 vars map 获取值,支持嵌套变量名
|
||||
if val, ok := data.GetByPath(varName); ok {
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
|
||||
// fallback: 使用 getter fn 获取值
|
||||
if r.getter != nil {
|
||||
if val, ok := r.getter(varName); ok {
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
return matched
|
||||
})
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package textutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gookit/goutil"
|
||||
"github.com/gookit/goutil/fsutil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
"github.com/gookit/goutil/x/basefn"
|
||||
)
|
||||
|
||||
var builtInFuncs = template.FuncMap{
|
||||
// don't escape content
|
||||
"raw": func(s string) string {
|
||||
return s
|
||||
},
|
||||
"trim": strings.TrimSpace,
|
||||
// lower first char
|
||||
"lcFirst": strutil.LowerFirst,
|
||||
// upper first char
|
||||
"upFirst": strutil.UpperFirst,
|
||||
// upper case
|
||||
"upper": strings.ToUpper,
|
||||
// lower case
|
||||
"lower": strings.ToLower,
|
||||
// cut sub-string
|
||||
"substr": strutil.Substr,
|
||||
// default value
|
||||
"default": func(v, defVal any) string {
|
||||
if goutil.IsEmpty(v) {
|
||||
return strutil.SafeString(defVal)
|
||||
}
|
||||
return strutil.SafeString(v)
|
||||
},
|
||||
// join strings
|
||||
"join": func(ss []string, sep string) string {
|
||||
return strings.Join(ss, sep)
|
||||
},
|
||||
}
|
||||
|
||||
// TextRenderOpt render text template options
|
||||
type TextRenderOpt struct {
|
||||
// Output use custom output writer
|
||||
Output io.Writer
|
||||
// Funcs add custom template functions
|
||||
Funcs template.FuncMap
|
||||
}
|
||||
|
||||
// RenderOptFn render option func
|
||||
type RenderOptFn func(opt *TextRenderOpt)
|
||||
|
||||
// NewRenderOpt create a new render options
|
||||
func NewRenderOpt(optFns []RenderOptFn) *TextRenderOpt {
|
||||
opt := &TextRenderOpt{}
|
||||
for _, fn := range optFns {
|
||||
fn(opt)
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// RenderGoTpl render input text or template file.
|
||||
func RenderGoTpl(input string, data any, optFns ...RenderOptFn) string {
|
||||
opt := NewRenderOpt(optFns)
|
||||
|
||||
t := template.New("text-renderer")
|
||||
t.Funcs(builtInFuncs)
|
||||
if len(opt.Funcs) > 0 {
|
||||
t.Funcs(opt.Funcs)
|
||||
}
|
||||
|
||||
if !strings.Contains(input, "{{") && fsutil.IsFile(input) {
|
||||
template.Must(t.ParseFiles(input))
|
||||
} else {
|
||||
template.Must(t.Parse(input))
|
||||
}
|
||||
|
||||
// use custom output writer
|
||||
if opt.Output != nil {
|
||||
basefn.MustOK(t.Execute(opt.Output, data))
|
||||
return "" // return empty string
|
||||
}
|
||||
|
||||
// use buffer receive rendered content
|
||||
buf := new(bytes.Buffer)
|
||||
basefn.MustOK(t.Execute(buf, data))
|
||||
return buf.String()
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Package textutil provide some extensions text handle util functions.
|
||||
package textutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// ReplaceVars by regex replace given tpl vars.
|
||||
//
|
||||
// If a format is empty, will use {const DefaultVarFormat}
|
||||
func ReplaceVars(text string, vars map[string]any, format string) string {
|
||||
return NewVarReplacer(format).Replace(text, vars)
|
||||
}
|
||||
|
||||
// RenderSMap by regex replacement given tpl vars.
|
||||
//
|
||||
// If a format is empty, will use {const DefaultVarFormat}
|
||||
func RenderSMap(text string, vars map[string]string, format string) string {
|
||||
return NewVarReplacer(format).RenderSimple(text, vars)
|
||||
}
|
||||
|
||||
// IsMatchAll keywords in the give text string.
|
||||
//
|
||||
// TIP: can use ^ for exclude match.
|
||||
func IsMatchAll(s string, keywords []string) bool {
|
||||
return strutil.SimpleMatch(s, keywords)
|
||||
}
|
||||
|
||||
// ParseInlineINI parse config string to string-map. it's like INI format contents.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// eg: "name=val0;shorts=i;required=true;desc=a message"
|
||||
// =>
|
||||
// {name: val0, shorts: i, required: true, desc: a message}
|
||||
func ParseInlineINI(tagVal string, keys ...string) (mp maputil.SMap, err error) {
|
||||
ss := strutil.Split(tagVal, ";")
|
||||
ln := len(ss)
|
||||
if ln == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mp = make(maputil.SMap, ln)
|
||||
for _, s := range ss {
|
||||
if !strings.ContainsRune(s, '=') {
|
||||
err = fmt.Errorf("parse inline config error: must match `KEY=VAL`")
|
||||
return
|
||||
}
|
||||
|
||||
key, val := strutil.TrimCut(s, "=")
|
||||
if len(keys) > 0 && !arrutil.StringsHas(keys, key) {
|
||||
err = fmt.Errorf("parse inline config error: invalid key name %q", key)
|
||||
return
|
||||
}
|
||||
|
||||
mp[key] = val
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseSimpleINI parse simple multiline config string to a string-map.
|
||||
// Can use to parse simple INI or dotenv file contents.
|
||||
//
|
||||
// NOTE:
|
||||
//
|
||||
// - it's like INI format contents.
|
||||
// - support comments line with: "#", ";", "//"
|
||||
// - support inline comments with: " #" eg: name=tom # a comments
|
||||
// - DON'T support submap parse.
|
||||
func ParseSimpleINI(text string) (mp maputil.SMap, err error) {
|
||||
return comfunc.ParseEnvLines(text, comfunc.ParseEnvLineOption{})
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package textutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/arrutil"
|
||||
"github.com/gookit/goutil/internal/varexpr"
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
// DefaultVarFormat var template
|
||||
const DefaultVarFormat = "{{,}}"
|
||||
|
||||
// FallbackFn type
|
||||
type FallbackFn = func(name string) (val string, ok bool)
|
||||
|
||||
// VarReplacer struct
|
||||
type VarReplacer struct {
|
||||
init bool
|
||||
|
||||
Left, Right string
|
||||
lLen, rLen int
|
||||
|
||||
varReg *regexp.Regexp
|
||||
// flatten sub map in vars. default: true
|
||||
//
|
||||
// eg: {name: {a: 1, b: 2}} => {name.a: 1, name.b: 2}
|
||||
flatSubs bool
|
||||
// do parse env value in var-value and tpl var-name. default: false
|
||||
parseEnv bool
|
||||
// do parse default value. default: false
|
||||
//
|
||||
// eg: {{ name | inhere }}
|
||||
parseDef bool
|
||||
// keepMissVars list.
|
||||
//
|
||||
// default: False - will clear on each replacement
|
||||
keepMissVars bool
|
||||
// missing vars list
|
||||
missVars []string
|
||||
// NotFound hook func. on var-name not found
|
||||
NotFound FallbackFn
|
||||
// RenderFn custom render func
|
||||
RenderFn func(s string, vs map[string]string) string
|
||||
}
|
||||
|
||||
// NewVarReplacer instance. default format is: DefaultVarFormat
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// rpl := NewVarReplacer("{{,}}") // access var: {{ var }}, {{ top.sub }}
|
||||
// // or
|
||||
// rpl := NewVarReplacer("$") // access var: $var, $top.sub
|
||||
func NewVarReplacer(format string, opFns ...func(vp *VarReplacer)) *VarReplacer {
|
||||
vp := &VarReplacer{flatSubs: true}
|
||||
for _, fn := range opFns {
|
||||
fn(vp)
|
||||
}
|
||||
return vp.WithFormat(format)
|
||||
}
|
||||
|
||||
// NewFullReplacer instance. will enable parse env and parse default.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// rpl := NewFullReplacer("{{,}}")
|
||||
func NewFullReplacer(format string) *VarReplacer {
|
||||
return NewVarReplacer(format, func(vp *VarReplacer) {
|
||||
vp.WithParseEnv().WithParseDefault().KeepMissingVars()
|
||||
})
|
||||
}
|
||||
|
||||
// DisableFlatten on the input vars map
|
||||
func (r *VarReplacer) DisableFlatten() *VarReplacer {
|
||||
r.flatSubs = false
|
||||
return r
|
||||
}
|
||||
|
||||
// KeepMissingVars on the replacement handle
|
||||
func (r *VarReplacer) KeepMissingVars() *VarReplacer {
|
||||
r.keepMissVars = true
|
||||
return r
|
||||
}
|
||||
|
||||
// WithParseDefault value on the input template contents. eg: {{ name | inhere }}
|
||||
func (r *VarReplacer) WithParseDefault() *VarReplacer {
|
||||
r.parseDef = true
|
||||
return r
|
||||
}
|
||||
|
||||
// WithParseEnv on the input vars value
|
||||
func (r *VarReplacer) WithParseEnv() *VarReplacer {
|
||||
r.parseEnv = true
|
||||
return r
|
||||
}
|
||||
|
||||
// OnNotFound var handle func
|
||||
func (r *VarReplacer) OnNotFound(fn FallbackFn) *VarReplacer {
|
||||
r.NotFound = fn
|
||||
return r
|
||||
}
|
||||
|
||||
// WithFormat custom var template
|
||||
func (r *VarReplacer) WithFormat(format string) *VarReplacer {
|
||||
r.Left, r.Right = strutil.QuietCut(strutil.OrElse(format, DefaultVarFormat), ",")
|
||||
r.Init()
|
||||
return r
|
||||
}
|
||||
|
||||
// Init var replacer
|
||||
func (r *VarReplacer) Init() {
|
||||
if !r.init {
|
||||
r.lLen, r.rLen = len(r.Left), len(r.Right)
|
||||
if r.Right != "" {
|
||||
r.varReg = regexp.MustCompile(regexp.QuoteMeta(r.Left) + `([\w\s\|.-]+)` + regexp.QuoteMeta(r.Right))
|
||||
} else {
|
||||
// no right tag. eg: $name, $user.age
|
||||
r.varReg = regexp.MustCompile(regexp.QuoteMeta(r.Left) + `(\w[\w-]*(?:\.[\w-]+)*)`)
|
||||
}
|
||||
|
||||
r.init = true
|
||||
}
|
||||
}
|
||||
|
||||
// ParseVars parse the text contents and collect vars
|
||||
func (r *VarReplacer) ParseVars(s string) []string {
|
||||
ss := arrutil.StringsMap(r.varReg.FindAllString(s, -1), func(val string) string {
|
||||
return strings.TrimSpace(val[r.lLen : len(val)-r.rLen])
|
||||
})
|
||||
|
||||
return arrutil.Unique(ss)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if !r.parseDef && len(tplVars) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
r.Init()
|
||||
|
||||
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) {
|
||||
if val.Kind() == reflect.String {
|
||||
if r.parseEnv {
|
||||
varMap[path] = varexpr.SafeParse(val.String())
|
||||
} else {
|
||||
varMap[path] = val.String()
|
||||
}
|
||||
} else {
|
||||
varMap[path] = strutil.QuietString(val.Interface())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
varMap = maputil.ToStringMap(tplVars)
|
||||
}
|
||||
|
||||
return r.doReplace(s, varMap)
|
||||
}
|
||||
|
||||
// ReplaceSMap string-map vars in the text contents
|
||||
func (r *VarReplacer) ReplaceSMap(s string, varMap map[string]string) string {
|
||||
return r.RenderSimple(s, varMap)
|
||||
}
|
||||
|
||||
// RenderSimple string-map vars in the text contents. alias of ReplaceSMap()
|
||||
func (r *VarReplacer) RenderSimple(s string, varMap map[string]string) string {
|
||||
if len(varMap) == 0 || !strings.Contains(s, r.Left) {
|
||||
return s
|
||||
}
|
||||
|
||||
if r.parseEnv {
|
||||
for name, val := range varMap {
|
||||
if strings.Contains(val, "${") {
|
||||
varMap[name] = varexpr.SafeParse(val)
|
||||
} else {
|
||||
varMap[name] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.Init()
|
||||
return r.doReplace(s, varMap)
|
||||
}
|
||||
|
||||
// MissVars list
|
||||
func (r *VarReplacer) MissVars() []string {
|
||||
return r.missVars
|
||||
}
|
||||
|
||||
// ResetMissVars list
|
||||
func (r *VarReplacer) ResetMissVars() {
|
||||
r.missVars = make([]string, 0)
|
||||
}
|
||||
|
||||
// Replace string-map vars in the text contents
|
||||
func (r *VarReplacer) doReplace(s string, varMap map[string]string) string {
|
||||
if !r.keepMissVars {
|
||||
r.missVars = make([]string, 0) // clear on each replacement
|
||||
}
|
||||
|
||||
// use custom render func
|
||||
if r.RenderFn != nil {
|
||||
return r.RenderFn(s, varMap)
|
||||
}
|
||||
|
||||
return r.varReg.ReplaceAllStringFunc(s, func(sub string) string {
|
||||
name := strings.TrimSpace(sub[r.lLen : len(sub)-r.rLen])
|
||||
|
||||
var defVal string
|
||||
if r.parseDef && strings.ContainsRune(name, '|') {
|
||||
name, defVal = strutil.TrimCut(name, "|")
|
||||
}
|
||||
|
||||
if val, ok := varMap[name]; ok {
|
||||
return val
|
||||
}
|
||||
if r.parseEnv && strutil.IsEnvName(name) {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// has custom not found handle func
|
||||
if r.NotFound != nil {
|
||||
if val, ok := r.NotFound(name); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
if len(defVal) > 0 {
|
||||
return defVal
|
||||
}
|
||||
r.missVars = append(r.missVars, name)
|
||||
return sub
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user