package stdlib import ( "fmt" "regexp" "strconv" "strings" "unicode/utf8" "github.com/d5/tengo/v2" ) var textModule = map[string]tengo.Object{ "re_match": &tengo.UserFunction{ Name: "re_match", Value: textREMatch, }, // re_match(pattern, text) => bool/error "re_find": &tengo.UserFunction{ Name: "re_find", Value: textREFind, }, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined "re_replace": &tengo.UserFunction{ Name: "re_replace", Value: textREReplace, }, // re_replace(pattern, text, repl) => string/error "re_split": &tengo.UserFunction{ Name: "re_split", Value: textRESplit, }, // re_split(pattern, text, count) => [string]/error "re_compile": &tengo.UserFunction{ Name: "re_compile", Value: textRECompile, }, // re_compile(pattern) => Regexp/error "compare": &tengo.UserFunction{ Name: "compare", Value: FuncASSRI(strings.Compare), }, // compare(a, b) => int "contains": &tengo.UserFunction{ Name: "contains", Value: FuncASSRB(strings.Contains), }, // contains(s, substr) => bool "contains_any": &tengo.UserFunction{ Name: "contains_any", Value: FuncASSRB(strings.ContainsAny), }, // contains_any(s, chars) => bool "count": &tengo.UserFunction{ Name: "count", Value: FuncASSRI(strings.Count), }, // count(s, substr) => int "equal_fold": &tengo.UserFunction{ Name: "equal_fold", Value: FuncASSRB(strings.EqualFold), }, // "equal_fold(s, t) => bool "fields": &tengo.UserFunction{ Name: "fields", Value: FuncASRSs(strings.Fields), }, // fields(s) => [string] "has_prefix": &tengo.UserFunction{ Name: "has_prefix", Value: FuncASSRB(strings.HasPrefix), }, // has_prefix(s, prefix) => bool "has_suffix": &tengo.UserFunction{ Name: "has_suffix", Value: FuncASSRB(strings.HasSuffix), }, // has_suffix(s, suffix) => bool "index": &tengo.UserFunction{ Name: "index", Value: FuncASSRI(strings.Index), }, // index(s, substr) => int "index_any": &tengo.UserFunction{ Name: "index_any", Value: FuncASSRI(strings.IndexAny), }, // index_any(s, chars) => int "join": &tengo.UserFunction{ Name: "join", Value: textJoin, }, // join(arr, sep) => string "last_index": &tengo.UserFunction{ Name: "last_index", Value: FuncASSRI(strings.LastIndex), }, // last_index(s, substr) => int "last_index_any": &tengo.UserFunction{ Name: "last_index_any", Value: FuncASSRI(strings.LastIndexAny), }, // last_index_any(s, chars) => int "repeat": &tengo.UserFunction{ Name: "repeat", Value: textRepeat, }, // repeat(s, count) => string "replace": &tengo.UserFunction{ Name: "replace", Value: textReplace, }, // replace(s, old, new, n) => string "substr": &tengo.UserFunction{ Name: "substr", Value: textSubstring, }, // substr(s, lower, upper) => string "split": &tengo.UserFunction{ Name: "split", Value: FuncASSRSs(strings.Split), }, // split(s, sep) => [string] "split_after": &tengo.UserFunction{ Name: "split_after", Value: FuncASSRSs(strings.SplitAfter), }, // split_after(s, sep) => [string] "split_after_n": &tengo.UserFunction{ Name: "split_after_n", Value: FuncASSIRSs(strings.SplitAfterN), }, // split_after_n(s, sep, n) => [string] "split_n": &tengo.UserFunction{ Name: "split_n", Value: FuncASSIRSs(strings.SplitN), }, // split_n(s, sep, n) => [string] "title": &tengo.UserFunction{ Name: "title", Value: FuncASRS(strings.Title), }, // title(s) => string "to_lower": &tengo.UserFunction{ Name: "to_lower", Value: FuncASRS(strings.ToLower), }, // to_lower(s) => string "to_title": &tengo.UserFunction{ Name: "to_title", Value: FuncASRS(strings.ToTitle), }, // to_title(s) => string "to_upper": &tengo.UserFunction{ Name: "to_upper", Value: FuncASRS(strings.ToUpper), }, // to_upper(s) => string "pad_left": &tengo.UserFunction{ Name: "pad_left", Value: textPadLeft, }, // pad_left(s, pad_len, pad_with) => string "pad_right": &tengo.UserFunction{ Name: "pad_right", Value: textPadRight, }, // pad_right(s, pad_len, pad_with) => string "trim": &tengo.UserFunction{ Name: "trim", Value: FuncASSRS(strings.Trim), }, // trim(s, cutset) => string "trim_left": &tengo.UserFunction{ Name: "trim_left", Value: FuncASSRS(strings.TrimLeft), }, // trim_left(s, cutset) => string "trim_prefix": &tengo.UserFunction{ Name: "trim_prefix", Value: FuncASSRS(strings.TrimPrefix), }, // trim_prefix(s, prefix) => string "trim_right": &tengo.UserFunction{ Name: "trim_right", Value: FuncASSRS(strings.TrimRight), }, // trim_right(s, cutset) => string "trim_space": &tengo.UserFunction{ Name: "trim_space", Value: FuncASRS(strings.TrimSpace), }, // trim_space(s) => string "trim_suffix": &tengo.UserFunction{ Name: "trim_suffix", Value: FuncASSRS(strings.TrimSuffix), }, // trim_suffix(s, suffix) => string "atoi": &tengo.UserFunction{ Name: "atoi", Value: FuncASRIE(strconv.Atoi), }, // atoi(str) => int/error "format_bool": &tengo.UserFunction{ Name: "format_bool", Value: textFormatBool, }, // format_bool(b) => string "format_float": &tengo.UserFunction{ Name: "format_float", Value: textFormatFloat, }, // format_float(f, fmt, prec, bits) => string "format_int": &tengo.UserFunction{ Name: "format_int", Value: textFormatInt, }, // format_int(i, base) => string "itoa": &tengo.UserFunction{ Name: "itoa", Value: FuncAIRS(strconv.Itoa), }, // itoa(i) => string "parse_bool": &tengo.UserFunction{ Name: "parse_bool", Value: textParseBool, }, // parse_bool(str) => bool/error "parse_float": &tengo.UserFunction{ Name: "parse_float", Value: textParseFloat, }, // parse_float(str, bits) => float/error "parse_int": &tengo.UserFunction{ Name: "parse_int", Value: textParseInt, }, // parse_int(str, base, bits) => int/error "quote": &tengo.UserFunction{ Name: "quote", Value: FuncASRS(strconv.Quote), }, // quote(str) => string "unquote": &tengo.UserFunction{ Name: "unquote", Value: FuncASRSE(strconv.Unquote), }, // unquote(str) => string/error } func textREMatch(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } matched, err := regexp.MatchString(s1, s2) if err != nil { ret = wrapError(err) return } if matched { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func textREFind(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs != 2 && numArgs != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } if numArgs < 3 { m := re.FindStringSubmatchIndex(s2) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for i := 0; i < len(m); i += 2 { arr.Value = append(arr.Value, &tengo.ImmutableMap{Value: map[string]tengo.Object{ "text": &tengo.String{Value: s2[m[i]:m[i+1]]}, "begin": &tengo.Int{Value: int64(m[i])}, "end": &tengo.Int{Value: int64(m[i+1])}, }}) } ret = &tengo.Array{Value: []tengo.Object{arr}} return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } m := re.FindAllStringSubmatchIndex(s2, i3) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for _, m := range m { subMatch := &tengo.Array{} for i := 0; i < len(m); i += 2 { subMatch.Value = append(subMatch.Value, &tengo.ImmutableMap{Value: map[string]tengo.Object{ "text": &tengo.String{Value: s2[m[i]:m[i+1]]}, "begin": &tengo.Int{Value: int64(m[i])}, "end": &tengo.Int{Value: int64(m[i+1])}, }}) } arr.Value = append(arr.Value, subMatch) } ret = arr return } func textREReplace(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s3, ok := tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) } else { s, ok := doTextRegexpReplace(re, s2, s3) if !ok { return nil, tengo.ErrStringLimit } ret = &tengo.String{Value: s} } return } func textRESplit(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs != 2 && numArgs != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } var i3 = -1 if numArgs > 2 { i3, ok = tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) return } arr := &tengo.Array{} for _, s := range re.Split(s2, i3) { arr.Value = append(arr.Value, &tengo.String{Value: s}) } ret = arr return } func textRECompile(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) } else { ret = makeTextRegexp(re) } return } func textReplace(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 4 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s3, ok := tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } s, ok := doTextReplace(s1, s2, s3, i4) if !ok { err = tengo.ErrStringLimit return } ret = &tengo.String{Value: s} return } func textSubstring(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } strlen := len(s1) i3 := strlen if argslen == 3 { i3, ok = tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } } if i2 > i3 { err = tengo.ErrInvalidIndexType return } if i2 < 0 { i2 = 0 } else if i2 > strlen { i2 = strlen } if i3 < 0 { i3 = 0 } else if i3 > strlen { i3 = strlen } ret = &tengo.String{Value: s1[i2:i3]} return } func textPadLeft(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } if i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } sLen := len(s1) if sLen >= i2 { ret = &tengo.String{Value: s1} return } s3 := " " if argslen == 3 { s3, ok = tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } } padStrLen := len(s3) if padStrLen == 0 { ret = &tengo.String{Value: s1} return } padCount := ((i2 - padStrLen) / padStrLen) + 1 retStr := strings.Repeat(s3, padCount) + s1 ret = &tengo.String{Value: retStr[len(retStr)-i2:]} return } func textPadRight(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } if i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } sLen := len(s1) if sLen >= i2 { ret = &tengo.String{Value: s1} return } s3 := " " if argslen == 3 { s3, ok = tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } } padStrLen := len(s3) if padStrLen == 0 { ret = &tengo.String{Value: s1} return } padCount := ((i2 - padStrLen) / padStrLen) + 1 retStr := s1 + strings.Repeat(s3, padCount) ret = &tengo.String{Value: retStr[:i2]} return } func textRepeat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } if len(s1)*i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: strings.Repeat(s1, i2)}, nil } func textJoin(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } var slen int var ss1 []string switch arg0 := args[0].(type) { case *tengo.Array: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } slen += len(as) ss1 = append(ss1, as) } case *tengo.ImmutableArray: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } slen += len(as) ss1 = append(ss1, as) } default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } // make sure output length does not exceed the limit if slen+len(s2)*(len(ss1)-1) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: strings.Join(ss1, s2)}, nil } func textFormatBool(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } b1, ok := args[0].(*tengo.Bool) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bool", Found: args[0].TypeName(), } return } if b1 == tengo.TrueValue { ret = &tengo.String{Value: "true"} } else { ret = &tengo.String{Value: "false"} } return } func textFormatFloat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 4 { err = tengo.ErrWrongNumArguments return } f1, ok := args[0].(*tengo.Float) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } ret = &tengo.String{Value: strconv.FormatFloat(f1.Value, s2[0], i3, i4)} return } func textFormatInt(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } i1, ok := args[0].(*tengo.Int) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } ret = &tengo.String{Value: strconv.FormatInt(i1.Value, i2)} return } func textParseBool(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } parsed, err := strconv.ParseBool(s1.Value) if err != nil { ret = wrapError(err) return } if parsed { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func textParseFloat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } parsed, err := strconv.ParseFloat(s1.Value, i2) if err != nil { ret = wrapError(err) return } ret = &tengo.Float{Value: parsed} return } func textParseInt(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } parsed, err := strconv.ParseInt(s1.Value, i2, i3) if err != nil { ret = wrapError(err) return } ret = &tengo.Int{Value: parsed} return } // Modified implementation of strings.Replace // to limit the maximum length of output string. func doTextReplace(s, old, new string, n int) (string, bool) { if old == new || n == 0 { return s, true // avoid allocation } // Compute number of replacements. if m := strings.Count(s, old); m == 0 { return s, true // avoid allocation } else if n < 0 || m < n { n = m } // Apply replacements to buffer. t := make([]byte, len(s)+n*(len(new)-len(old))) w := 0 start := 0 for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { _, wid := utf8.DecodeRuneInString(s[start:]) j += wid } } else { j += strings.Index(s[start:], old) } ssj := s[start:j] if w+len(ssj)+len(new) > tengo.MaxStringLen { return "", false } w += copy(t[w:], ssj) w += copy(t[w:], new) start = j + len(old) } ss := s[start:] if w+len(ss) > tengo.MaxStringLen { return "", false } w += copy(t[w:], ss) return string(t[0:w]), true }