From 0012a7089d1daa48d5b1004b2cd11357caab5773 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 17 Jul 2025 12:34:36 -0500 Subject: [PATCH] update json, math and string modules --- modules/json/json.go | 80 ++- modules/math/math.go | 128 ++--- modules/string/string.go | 1143 +++++++++---------------------------- modules/string/string.lua | 727 ++++++++--------------- 4 files changed, 581 insertions(+), 1497 deletions(-) diff --git a/modules/json/json.go b/modules/json/json.go index 1936c15..06b2560 100644 --- a/modules/json/json.go +++ b/modules/json/json.go @@ -5,51 +5,41 @@ import ( "github.com/goccy/go-json" ) -// GetJSONFunctions returns all JSON-related Go functions -func GetJSONFunctions() map[string]luajit.GoFunction { +func GetFunctionList() map[string]luajit.GoFunction { return map[string]luajit.GoFunction{ - "json_encode": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("json_encode: %v", err) - } - - value, err := s.ToValue(1) - if err != nil { - return s.PushError("json_encode: failed to read value: %v", err) - } - - data, err := json.Marshal(value) - if err != nil { - return s.PushError("json_encode: %v", err) - } - - s.PushString(string(data)) - return 1 - }, - - "json_decode": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("json_decode: %v", err) - } - - jsonStr, err := s.SafeToString(1) - if err != nil { - return s.PushError("json_decode: input must be a string") - } - - var result any - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - // Return nil and error string instead of PushError for JSON parsing errors - s.PushNil() - s.PushString(err.Error()) - return 2 - } - - if err := s.PushValue(result); err != nil { - return s.PushError("json_decode: failed to push result: %v", err) - } - - return 1 - }, + "json_encode": json_encode, + "json_decode": json_decode, } } + +func json_encode(s *luajit.State) int { + value, err := s.ToValue(1) + if err != nil { + s.PushNil() + s.PushString("failed to read value") + return 2 + } + + data, err := json.Marshal(value) + if err != nil { + s.PushNil() + s.PushString("encoding failed") + return 2 + } + + s.PushString(string(data)) + return 1 +} + +func json_decode(s *luajit.State) int { + jsonStr := s.ToString(1) + var result any + if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { + s.PushNil() + s.PushString("invalid JSON") + return 2 + } + + s.PushValue(result) + return 1 +} diff --git a/modules/math/math.go b/modules/math/math.go index 07b2bff..1ad8146 100644 --- a/modules/math/math.go +++ b/modules/math/math.go @@ -2,84 +2,56 @@ package math import luajit "git.sharkk.net/Sky/LuaJIT-to-Go" -// GetMathFunctions returns all math-related Go functions -func GetMathFunctions() map[string]luajit.GoFunction { +func GetFunctionList() map[string]luajit.GoFunction { return map[string]luajit.GoFunction{ - "math_factorial": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("math_factorial: %v", err) - } - - n, err := s.SafeToNumber(1) - if err != nil || n < 0 || n != float64(int(n)) { - return s.PushError("math_factorial: argument must be a non-negative integer") - } - - if n > 170 { - return s.PushError("math_factorial: argument too large (max 170)") - } - - result := 1.0 - for i := 2; i <= int(n); i++ { - result *= float64(i) - } - - s.PushNumber(result) - return 1 - }, - - "math_gcd": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("math_gcd: %v", err) - } - - a, err := s.SafeToNumber(1) - if err != nil || a != float64(int(a)) { - return s.PushError("math_gcd: first argument must be an integer") - } - - b, err := s.SafeToNumber(2) - if err != nil || b != float64(int(b)) { - return s.PushError("math_gcd: second argument must be an integer") - } - - ia, ib := int(a), int(b) - for ib != 0 { - ia, ib = ib, ia%ib - } - - s.PushNumber(float64(ia)) - return 1 - }, - - "math_lcm": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("math_lcm: %v", err) - } - - a, err := s.SafeToNumber(1) - if err != nil || a != float64(int(a)) { - return s.PushError("math_lcm: first argument must be an integer") - } - - b, err := s.SafeToNumber(2) - if err != nil || b != float64(int(b)) { - return s.PushError("math_lcm: second argument must be an integer") - } - - ia, ib := int(a), int(b) - - // Calculate GCD - gcd := func(x, y int) int { - for y != 0 { - x, y = y, x%y - } - return x - } - - result := ia * ib / gcd(ia, ib) - s.PushNumber(float64(result)) - return 1 - }, + "math_factorial": math_factorial, + "math_gcd": math_gcd, + "math_lcm": math_lcm, } } + +func math_factorial(s *luajit.State) int { + n := s.ToNumber(1) + if n < 0 || n != float64(int(n)) || n > 170 { + s.PushNil() + s.PushString("invalid argument") + return 2 + } + + result := 1.0 + for i := 2; i <= int(n); i++ { + result *= float64(i) + } + + s.PushNumber(result) + return 1 +} + +func math_gcd(s *luajit.State) int { + a := int(s.ToNumber(1)) + b := int(s.ToNumber(2)) + + for b != 0 { + a, b = b, a%b + } + + s.PushNumber(float64(a)) + return 1 +} + +func math_lcm(s *luajit.State) int { + a := int(s.ToNumber(1)) + b := int(s.ToNumber(2)) + + // Calculate GCD + gcd := func(x, y int) int { + for y != 0 { + x, y = y, x%y + } + return x + } + + result := a * b / gcd(a, b) + s.PushNumber(float64(result)) + return 1 +} diff --git a/modules/string/string.go b/modules/string/string.go index e2c64f0..68ab992 100644 --- a/modules/string/string.go +++ b/modules/string/string.go @@ -1,894 +1,289 @@ package string import ( - "fmt" "math/rand" "regexp" - "strconv" "strings" "time" - "unicode" "unicode/utf8" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) const ( maxStringLength = 10_000_000 // 10MB limit for safety - maxRepeatCount = 1_000_000 // Prevent excessive memory usage maxRandomLength = 100_000 // Reasonable limit for random strings ) -func validateStringLength(s string) error { - if len(s) > maxStringLength { - return fmt.Errorf("string too large (max %d bytes)", maxStringLength) - } - return nil -} - -func GetStringFunctions() map[string]luajit.GoFunction { +func GetFunctionList() map[string]luajit.GoFunction { return map[string]luajit.GoFunction{ - "string_split": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_split: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_split: first argument must be a string") - } - sep, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_split: second argument must be a string") - } - - if err := validateStringLength(str); err != nil { - return s.PushError("string_split: %v", err) - } - - // Handle empty separator - split into characters - if sep == "" { - runes := []rune(str) - parts := make([]string, len(runes)) - for i, r := range runes { - parts[i] = string(r) - } - if err := s.PushValue(parts); err != nil { - return s.PushError("string_split: failed to push result: %v", err) - } - return 1 - } - - parts := strings.Split(str, sep) - if err := s.PushValue(parts); err != nil { - return s.PushError("string_split: failed to push result: %v", err) - } - return 1 - }, - - "string_join": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_join: %v", err) - } - arr, err := s.SafeToTable(1) - if err != nil { - return s.PushError("string_join: first argument must be a table") - } - sep, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_join: second argument must be a string") - } - - var parts []string - switch v := arr.(type) { - case []string: - parts = v - case []any: - parts = make([]string, len(v)) - for i, val := range v { - if val == nil { - parts[i] = "" - } else { - parts[i] = fmt.Sprintf("%v", val) - } - } - case map[string]any: - if len(v) == 0 { - parts = []string{} - } else { - return s.PushError("string_join: first argument must be an array, not a map") - } - default: - return s.PushError("string_join: first argument must be an array") - } - - result := strings.Join(parts, sep) - if err := validateStringLength(result); err != nil { - return s.PushError("string_join: result %v", err) - } - - s.PushString(result) - return 1 - }, - - "string_trim": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_trim: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_trim: argument must be a string") - } - s.PushString(strings.TrimSpace(str)) - return 1 - }, - - "string_trim_left": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_trim_left: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_trim_left: first argument must be a string") - } - - if s.GetTop() >= 2 && !s.IsNil(2) { - cutset, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_trim_left: second argument must be a string") - } - s.PushString(strings.TrimLeft(str, cutset)) - } else { - s.PushString(strings.TrimLeftFunc(str, unicode.IsSpace)) - } - return 1 - }, - - "string_trim_right": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_trim_right: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_trim_right: first argument must be a string") - } - - if s.GetTop() >= 2 && !s.IsNil(2) { - cutset, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_trim_right: second argument must be a string") - } - s.PushString(strings.TrimRight(str, cutset)) - } else { - s.PushString(strings.TrimRightFunc(str, unicode.IsSpace)) - } - return 1 - }, - - "string_upper": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_upper: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_upper: argument must be a string") - } - s.PushString(strings.ToUpper(str)) - return 1 - }, - - "string_lower": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_lower: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_lower: argument must be a string") - } - s.PushString(strings.ToLower(str)) - return 1 - }, - - "string_title": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_title: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_title: argument must be a string") - } - caser := cases.Title(language.English, cases.NoLower) - s.PushString(caser.String(str)) - return 1 - }, - - "string_contains": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_contains: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_contains: first argument must be a string") - } - substr, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_contains: second argument must be a string") - } - s.PushBoolean(strings.Contains(str, substr)) - return 1 - }, - - "string_starts_with": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_starts_with: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_starts_with: first argument must be a string") - } - prefix, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_starts_with: second argument must be a string") - } - s.PushBoolean(strings.HasPrefix(str, prefix)) - return 1 - }, - - "string_ends_with": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_ends_with: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_ends_with: first argument must be a string") - } - suffix, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_ends_with: second argument must be a string") - } - s.PushBoolean(strings.HasSuffix(str, suffix)) - return 1 - }, - - "string_replace": func(s *luajit.State) int { - if err := s.CheckExactArgs(3); err != nil { - return s.PushError("string_replace: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_replace: first argument must be a string") - } - old, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_replace: second argument must be a string") - } - new, err := s.SafeToString(3) - if err != nil { - return s.PushError("string_replace: third argument must be a string") - } - - if old == "" { - return s.PushError("string_replace: cannot replace empty string") - } - - result := strings.ReplaceAll(str, old, new) - if err := validateStringLength(result); err != nil { - return s.PushError("string_replace: result %v", err) - } - - s.PushString(result) - return 1 - }, - - "string_replace_n": func(s *luajit.State) int { - if err := s.CheckExactArgs(4); err != nil { - return s.PushError("string_replace_n: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_replace_n: first argument must be a string") - } - old, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_replace_n: second argument must be a string") - } - new, err := s.SafeToString(3) - if err != nil { - return s.PushError("string_replace_n: third argument must be a string") - } - n, err := s.SafeToNumber(4) - if err != nil || n != float64(int(n)) || n < 0 { - return s.PushError("string_replace_n: fourth argument must be a non-negative integer") - } - - if old == "" { - return s.PushError("string_replace_n: cannot replace empty string") - } - - result := strings.Replace(str, old, new, int(n)) - s.PushString(result) - return 1 - }, - - "string_index": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_index: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_index: first argument must be a string") - } - substr, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_index: second argument must be a string") - } - - if substr == "" { - s.PushNumber(1) // Empty string found at position 1 - return 1 - } - - index := strings.Index(str, substr) - if index == -1 { - s.PushNumber(0) // Not found - } else { - s.PushNumber(float64(index + 1)) // Convert to 1-indexed - } - return 1 - }, - - "string_last_index": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_last_index: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_last_index: first argument must be a string") - } - substr, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_last_index: second argument must be a string") - } - - if substr == "" { - s.PushNumber(float64(utf8.RuneCountInString(str) + 1)) // Empty string at end - return 1 - } - - index := strings.LastIndex(str, substr) - if index == -1 { - s.PushNumber(0) // Not found - } else { - s.PushNumber(float64(index + 1)) // Convert to 1-indexed - } - return 1 - }, - - "string_count": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_count: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_count: first argument must be a string") - } - substr, err := s.SafeToString(2) - if err != nil { - return s.PushError("string_count: second argument must be a string") - } - - if substr == "" { - // Empty string matches at every position including boundaries - s.PushNumber(float64(utf8.RuneCountInString(str) + 1)) - return 1 - } - - count := strings.Count(str, substr) - s.PushNumber(float64(count)) - return 1 - }, - - "string_repeat": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("string_repeat: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_repeat: first argument must be a string") - } - count, err := s.SafeToNumber(2) - if err != nil || count < 0 || count != float64(int(count)) { - return s.PushError("string_repeat: second argument must be a non-negative integer") - } - - n := int(count) - if n == 0 { - s.PushString("") - return 1 - } - - // Check for potential overflow - if len(str) > 0 && n > maxRepeatCount/len(str) { - return s.PushError("string_repeat: result would be too large") - } - - result := strings.Repeat(str, n) - s.PushString(result) - return 1 - }, - - "string_reverse": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_reverse: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_reverse: argument must be a string") - } - - if !utf8.ValidString(str) { - return s.PushError("string_reverse: invalid UTF-8 string") - } - - runes := []rune(str) - for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { - runes[i], runes[j] = runes[j], runes[i] - } - s.PushString(string(runes)) - return 1 - }, - - "string_length": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_length: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_length: argument must be a string") - } - s.PushNumber(float64(utf8.RuneCountInString(str))) - return 1 - }, - - "string_byte_length": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_byte_length: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_byte_length: argument must be a string") - } - s.PushNumber(float64(len(str))) - return 1 - }, - - "string_lines": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_lines: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_lines: argument must be a string") - } - - // Handle different line endings - str = strings.ReplaceAll(str, "\r\n", "\n") - str = strings.ReplaceAll(str, "\r", "\n") - lines := strings.Split(str, "\n") - - if err := s.PushValue(lines); err != nil { - return s.PushError("string_lines: failed to push result: %v", err) - } - return 1 - }, - - "string_words": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_words: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_words: argument must be a string") - } - words := strings.Fields(str) - if err := s.PushValue(words); err != nil { - return s.PushError("string_words: failed to push result: %v", err) - } - return 1 - }, - - "string_pad_left": func(s *luajit.State) int { - if err := s.CheckMinArgs(2); err != nil { - return s.PushError("string_pad_left: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_pad_left: first argument must be a string") - } - width, err := s.SafeToNumber(2) - if err != nil || width != float64(int(width)) || width < 0 { - return s.PushError("string_pad_left: second argument must be a non-negative integer") - } - - padChar := " " - if s.GetTop() >= 3 && !s.IsNil(3) { - if p, err := s.SafeToString(3); err == nil && utf8.RuneCountInString(p) > 0 { - runes := []rune(p) - padChar = string(runes[0]) - } - } - - currentLen := utf8.RuneCountInString(str) - targetLen := int(width) - if currentLen >= targetLen { - s.PushString(str) - return 1 - } - - padLen := targetLen - currentLen - if padLen > maxRepeatCount { - return s.PushError("string_pad_left: padding too large") - } - - padding := strings.Repeat(padChar, padLen) - s.PushString(padding + str) - return 1 - }, - - "string_pad_right": func(s *luajit.State) int { - if err := s.CheckMinArgs(2); err != nil { - return s.PushError("string_pad_right: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_pad_right: first argument must be a string") - } - width, err := s.SafeToNumber(2) - if err != nil || width != float64(int(width)) || width < 0 { - return s.PushError("string_pad_right: second argument must be a non-negative integer") - } - - padChar := " " - if s.GetTop() >= 3 && !s.IsNil(3) { - if p, err := s.SafeToString(3); err == nil && utf8.RuneCountInString(p) > 0 { - runes := []rune(p) - padChar = string(runes[0]) - } - } - - currentLen := utf8.RuneCountInString(str) - targetLen := int(width) - if currentLen >= targetLen { - s.PushString(str) - return 1 - } - - padLen := targetLen - currentLen - if padLen > maxRepeatCount { - return s.PushError("string_pad_right: padding too large") - } - - padding := strings.Repeat(padChar, padLen) - s.PushString(str + padding) - return 1 - }, - - "string_slice": func(s *luajit.State) int { - if err := s.CheckMinArgs(2); err != nil { - return s.PushError("string_slice: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_slice: first argument must be a string") - } - start, err := s.SafeToNumber(2) - if err != nil || start != float64(int(start)) { - return s.PushError("string_slice: second argument must be an integer") - } - - if !utf8.ValidString(str) { - return s.PushError("string_slice: invalid UTF-8 string") - } - - runes := []rune(str) - length := len(runes) - startIdx := int(start) - 1 // Convert from 1-indexed to 0-indexed - - // Handle negative start index - if startIdx < 0 { - startIdx = 0 - } - if startIdx >= length { - s.PushString("") - return 1 - } - - endIdx := length - if s.GetTop() >= 3 && !s.IsNil(3) { - end, err := s.SafeToNumber(3) - if err == nil && end == float64(int(end)) { - endIdx = int(end) - // Handle negative end index (from end of string) - if endIdx < 0 { - endIdx = length + endIdx + 1 - } - if endIdx < 0 { - endIdx = 0 - } - if endIdx > length { - endIdx = length - } - } - } - - if startIdx >= endIdx { - s.PushString("") - return 1 - } - - s.PushString(string(runes[startIdx:endIdx])) - return 1 - }, - - "regex_match": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("regex_match: %v", err) - } - pattern, err := s.SafeToString(1) - if err != nil { - return s.PushError("regex_match: first argument must be a string") - } - str, err := s.SafeToString(2) - if err != nil { - return s.PushError("regex_match: second argument must be a string") - } - - re, err := regexp.Compile(pattern) - if err != nil { - s.PushBoolean(false) - return 1 - } - - s.PushBoolean(re.MatchString(str)) - return 1 - }, - - "regex_find": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("regex_find: %v", err) - } - pattern, err := s.SafeToString(1) - if err != nil { - return s.PushError("regex_find: first argument must be a string") - } - str, err := s.SafeToString(2) - if err != nil { - return s.PushError("regex_find: second argument must be a string") - } - - re, err := regexp.Compile(pattern) - if err != nil { - s.PushNil() - return 1 - } - - match := re.FindString(str) - if match == "" { - s.PushNil() - } else { - s.PushString(match) - } - return 1 - }, - - "regex_find_all": func(s *luajit.State) int { - if err := s.CheckExactArgs(2); err != nil { - return s.PushError("regex_find_all: %v", err) - } - pattern, err := s.SafeToString(1) - if err != nil { - return s.PushError("regex_find_all: first argument must be a string") - } - str, err := s.SafeToString(2) - if err != nil { - return s.PushError("regex_find_all: second argument must be a string") - } - - re, err := regexp.Compile(pattern) - if err != nil { - // Return empty array for invalid patterns - if err := s.PushValue([]string{}); err != nil { - return s.PushError("regex_find_all: failed to push result: %v", err) - } - return 1 - } - - matches := re.FindAllString(str, -1) - if matches == nil { - matches = []string{} // Return empty array instead of nil - } - - if err := s.PushValue(matches); err != nil { - return s.PushError("regex_find_all: failed to push result: %v", err) - } - return 1 - }, - - "regex_replace": func(s *luajit.State) int { - if err := s.CheckExactArgs(3); err != nil { - return s.PushError("regex_replace: %v", err) - } - pattern, err := s.SafeToString(1) - if err != nil { - return s.PushError("regex_replace: first argument must be a string") - } - str, err := s.SafeToString(2) - if err != nil { - return s.PushError("regex_replace: second argument must be a string") - } - replacement, err := s.SafeToString(3) - if err != nil { - return s.PushError("regex_replace: third argument must be a string") - } - - re, err := regexp.Compile(pattern) - if err != nil { - // Return original string for invalid patterns - s.PushString(str) - return 1 - } - - result := re.ReplaceAllString(str, replacement) - s.PushString(result) - return 1 - }, - - "string_to_number": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_to_number: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_to_number: argument must be a string") - } - - // Trim whitespace for more lenient parsing - str = strings.TrimSpace(str) - - // Try float first for more general parsing - if num, err := strconv.ParseFloat(str, 64); err == nil { - s.PushNumber(num) - return 1 - } - - s.PushNil() - return 1 - }, - - "string_is_numeric": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_is_numeric: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_is_numeric: argument must be a string") - } - - str = strings.TrimSpace(str) - if str == "" { - s.PushBoolean(false) - return 1 - } - - _, err1 := strconv.ParseFloat(str, 64) - s.PushBoolean(err1 == nil) - return 1 - }, - - "string_is_alpha": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_is_alpha: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_is_alpha: argument must be a string") - } - - if len(str) == 0 { - s.PushBoolean(false) - return 1 - } - - for _, r := range str { - if !unicode.IsLetter(r) { - s.PushBoolean(false) - return 1 - } - } - s.PushBoolean(true) - return 1 - }, - - "string_is_alphanumeric": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_is_alphanumeric: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_is_alphanumeric: argument must be a string") - } - - if len(str) == 0 { - s.PushBoolean(false) - return 1 - } - - for _, r := range str { - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - s.PushBoolean(false) - return 1 - } - } - s.PushBoolean(true) - return 1 - }, - - "random_string": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("random_string: %v", err) - } - length, err := s.SafeToNumber(1) - if err != nil || length != float64(int(length)) || length < 0 { - return s.PushError("random_string: first argument must be a non-negative integer") - } - - charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - if s.GetTop() >= 2 && !s.IsNil(2) { - if custom, err := s.SafeToString(2); err == nil && len(custom) > 0 { - charset = custom - } - } - - n := int(length) - if n == 0 { - s.PushString("") - return 1 - } - if n > maxRandomLength { - return s.PushError("random_string: length too large (max %d)", maxRandomLength) - } - - // Validate charset for UTF-8 - if !utf8.ValidString(charset) { - return s.PushError("random_string: charset must be valid UTF-8") - } - - charsetRunes := []rune(charset) - if len(charsetRunes) == 0 { - return s.PushError("random_string: charset cannot be empty") - } - - result := make([]rune, n) - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := range result { - result[i] = charsetRunes[rnd.Intn(len(charsetRunes))] - } - - s.PushString(string(result)) - return 1 - }, - - "string_is_valid_utf8": func(s *luajit.State) int { - if err := s.CheckMinArgs(1); err != nil { - return s.PushError("string_is_valid_utf8: %v", err) - } - str, err := s.SafeToString(1) - if err != nil { - return s.PushError("string_is_valid_utf8: argument must be a string") - } - s.PushBoolean(utf8.ValidString(str)) - return 1 - }, + "string_split": string_split, + "string_join": string_join, + "string_slice": string_slice, + "string_reverse": string_reverse, + "string_length": string_length, + "string_byte_length": string_byte_length, + "regex_match": regex_match, + "regex_find": regex_find, + "regex_find_all": regex_find_all, + "regex_replace": regex_replace, + "random_string": random_string, + "string_is_valid_utf8": string_is_valid_utf8, } } + +func string_split(s *luajit.State) int { + str := s.ToString(1) + sep := s.ToString(2) + + if len(str) > maxStringLength { + s.PushNil() + s.PushString("string too large") + return 2 + } + + // Handle empty separator - split into characters + if sep == "" { + runes := []rune(str) + parts := make([]string, len(runes)) + for i, r := range runes { + parts[i] = string(r) + } + s.PushValue(parts) + return 1 + } + + parts := strings.Split(str, sep) + s.PushValue(parts) + return 1 +} + +func string_join(s *luajit.State) int { + arr, err := s.ToValue(1) + if err != nil { + s.PushNil() + s.PushString("invalid array") + return 2 + } + sep := s.ToString(2) + + var parts []string + switch v := arr.(type) { + case []string: + parts = v + case []any: + parts = make([]string, len(v)) + for i, val := range v { + if val == nil { + parts[i] = "" + } else { + parts[i] = s.ToString(-1) // Convert via Lua + } + } + default: + s.PushNil() + s.PushString("not an array") + return 2 + } + + result := strings.Join(parts, sep) + if len(result) > maxStringLength { + s.PushNil() + s.PushString("result too large") + return 2 + } + + s.PushString(result) + return 1 +} + +func string_slice(s *luajit.State) int { + str := s.ToString(1) + start := int(s.ToNumber(2)) + + if !utf8.ValidString(str) { + s.PushNil() + s.PushString("invalid UTF-8") + return 2 + } + + runes := []rune(str) + length := len(runes) + startIdx := start - 1 // Convert from 1-indexed + + if startIdx < 0 { + startIdx = 0 + } + if startIdx >= length { + s.PushString("") + return 1 + } + + endIdx := length + if s.GetTop() >= 3 && !s.IsNil(3) { + end := int(s.ToNumber(3)) + if end < 0 { + endIdx = length + end + 1 + } else { + endIdx = end + } + if endIdx < 0 { + endIdx = 0 + } + if endIdx > length { + endIdx = length + } + } + + if startIdx >= endIdx { + s.PushString("") + return 1 + } + + s.PushString(string(runes[startIdx:endIdx])) + return 1 +} + +func string_reverse(s *luajit.State) int { + str := s.ToString(1) + + if !utf8.ValidString(str) { + s.PushNil() + s.PushString("invalid UTF-8") + return 2 + } + + runes := []rune(str) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + s.PushString(string(runes)) + return 1 +} + +func string_length(s *luajit.State) int { + str := s.ToString(1) + s.PushNumber(float64(utf8.RuneCountInString(str))) + return 1 +} + +func string_byte_length(s *luajit.State) int { + str := s.ToString(1) + s.PushNumber(float64(len(str))) + return 1 +} + +func regex_match(s *luajit.State) int { + pattern := s.ToString(1) + str := s.ToString(2) + + re, err := regexp.Compile(pattern) + if err != nil { + s.PushBoolean(false) + return 1 + } + + s.PushBoolean(re.MatchString(str)) + return 1 +} + +func regex_find(s *luajit.State) int { + pattern := s.ToString(1) + str := s.ToString(2) + + re, err := regexp.Compile(pattern) + if err != nil { + s.PushNil() + return 1 + } + + match := re.FindString(str) + if match == "" { + s.PushNil() + } else { + s.PushString(match) + } + return 1 +} + +func regex_find_all(s *luajit.State) int { + pattern := s.ToString(1) + str := s.ToString(2) + + re, err := regexp.Compile(pattern) + if err != nil { + s.PushValue([]string{}) + return 1 + } + + matches := re.FindAllString(str, -1) + if matches == nil { + matches = []string{} + } + + s.PushValue(matches) + return 1 +} + +func regex_replace(s *luajit.State) int { + pattern := s.ToString(1) + str := s.ToString(2) + replacement := s.ToString(3) + + re, err := regexp.Compile(pattern) + if err != nil { + s.PushString(str) + return 1 + } + + result := re.ReplaceAllString(str, replacement) + s.PushString(result) + return 1 +} + +func random_string(s *luajit.State) int { + length := int(s.ToNumber(1)) + if length < 0 || length > maxRandomLength { + s.PushNil() + s.PushString("invalid length") + return 2 + } + + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + if s.GetTop() >= 2 && !s.IsNil(2) { + charset = s.ToString(2) + } + + if length == 0 { + s.PushString("") + return 1 + } + + if !utf8.ValidString(charset) { + s.PushNil() + s.PushString("invalid charset") + return 2 + } + + charsetRunes := []rune(charset) + if len(charsetRunes) == 0 { + s.PushNil() + s.PushString("empty charset") + return 2 + } + + result := make([]rune, length) + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := range result { + result[i] = charsetRunes[rnd.Intn(len(charsetRunes))] + } + + s.PushString(string(result)) + return 1 +} + +func string_is_valid_utf8(s *luajit.State) int { + str := s.ToString(1) + s.PushBoolean(utf8.ValidString(str)) + return 1 +} diff --git a/modules/string/string.lua b/modules/string/string.lua index 507495e..22c178a 100644 --- a/modules/string/string.lua +++ b/modules/string/string.lua @@ -2,245 +2,253 @@ local str = {} --- Helper function to handle errors from Go functions -local function safe_call(func, ...) - local success, result = pcall(func, ...) - if not success then - error(result, 2) - end - return result -end - --- Helper to validate arguments -local function validate_string(s, func_name, arg_num) - if type(s) ~= "string" then - error(string.format("%s: argument %d must be a string, got %s", - func_name, arg_num or 1, type(s)), 3) - end -end - -local function validate_number(n, func_name, arg_num) - if type(n) ~= "number" then - error(string.format("%s: argument %d must be a number, got %s", - func_name, arg_num or 1, type(n)), 3) - end -end - -local function validate_table(t, func_name, arg_num) - if type(t) ~= "table" then - error(string.format("%s: argument %d must be a table, got %s", - func_name, arg_num or 1, type(t)), 3) - end -end - -- ====================================================================== --- BASIC STRING OPERATIONS +-- BASIC STRING OPERATIONS (Pure Lua) -- ====================================================================== function str.split(s, delimiter) - validate_string(s, "str.split", 1) - validate_string(delimiter, "str.split", 2) - return safe_call(moonshark.string_split, s, delimiter) + if type(s) ~= "string" then error("str.split: first argument must be a string", 2) end + if type(delimiter) ~= "string" then error("str.split: second argument must be a string", 2) end + return moonshark.string_split(s, delimiter) end function str.join(arr, separator) - validate_table(arr, "str.join", 1) - validate_string(separator, "str.join", 2) - return safe_call(moonshark.string_join, arr, separator) + if type(arr) ~= "table" then error("str.join: first argument must be a table", 2) end + if type(separator) ~= "string" then error("str.join: second argument must be a string", 2) end + return moonshark.string_join(arr, separator) end function str.trim(s) - validate_string(s, "str.trim") - return safe_call(moonshark.string_trim, s) + if type(s) ~= "string" then error("str.trim: argument must be a string", 2) end + return s:match("^%s*(.-)%s*$") end function str.trim_left(s, cutset) - validate_string(s, "str.trim_left", 1) - if cutset ~= nil then - validate_string(cutset, "str.trim_left", 2) + if type(s) ~= "string" then error("str.trim_left: first argument must be a string", 2) end + if cutset then + if type(cutset) ~= "string" then error("str.trim_left: second argument must be a string", 2) end + local pattern = "^[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*" + return s:gsub(pattern, "") + else + return s:match("^%s*(.*)") end - return safe_call(moonshark.string_trim_left, s, cutset) end function str.trim_right(s, cutset) - validate_string(s, "str.trim_right", 1) - if cutset ~= nil then - validate_string(cutset, "str.trim_right", 2) + if type(s) ~= "string" then error("str.trim_right: first argument must be a string", 2) end + if cutset then + if type(cutset) ~= "string" then error("str.trim_right: second argument must be a string", 2) end + local pattern = "[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*$" + return s:gsub(pattern, "") + else + return s:match("(.-)%s*$") end - return safe_call(moonshark.string_trim_right, s, cutset) end function str.upper(s) - validate_string(s, "str.upper") - return safe_call(moonshark.string_upper, s) + if type(s) ~= "string" then error("str.upper: argument must be a string", 2) end + return s:upper() end function str.lower(s) - validate_string(s, "str.lower") - return safe_call(moonshark.string_lower, s) + if type(s) ~= "string" then error("str.lower: argument must be a string", 2) end + return s:lower() end function str.title(s) - validate_string(s, "str.title") - return safe_call(moonshark.string_title, s) + if type(s) ~= "string" then error("str.title: argument must be a string", 2) end + return s:gsub("(%a)([%w_']*)", function(first, rest) + return first:upper() .. rest:lower() + end) end function str.contains(s, substr) - validate_string(s, "str.contains", 1) - validate_string(substr, "str.contains", 2) - return safe_call(moonshark.string_contains, s, substr) + if type(s) ~= "string" then error("str.contains: first argument must be a string", 2) end + if type(substr) ~= "string" then error("str.contains: second argument must be a string", 2) end + return s:find(substr, 1, true) ~= nil end function str.starts_with(s, prefix) - validate_string(s, "str.starts_with", 1) - validate_string(prefix, "str.starts_with", 2) - return safe_call(moonshark.string_starts_with, s, prefix) + if type(s) ~= "string" then error("str.starts_with: first argument must be a string", 2) end + if type(prefix) ~= "string" then error("str.starts_with: second argument must be a string", 2) end + return s:sub(1, #prefix) == prefix end function str.ends_with(s, suffix) - validate_string(s, "str.ends_with", 1) - validate_string(suffix, "str.ends_with", 2) - return safe_call(moonshark.string_ends_with, s, suffix) + if type(s) ~= "string" then error("str.ends_with: first argument must be a string", 2) end + if type(suffix) ~= "string" then error("str.ends_with: second argument must be a string", 2) end + return s:sub(-#suffix) == suffix end function str.replace(s, old, new) - validate_string(s, "str.replace", 1) - validate_string(old, "str.replace", 2) - validate_string(new, "str.replace", 3) - return safe_call(moonshark.string_replace, s, old, new) + if type(s) ~= "string" then error("str.replace: first argument must be a string", 2) end + if type(old) ~= "string" then error("str.replace: second argument must be a string", 2) end + if type(new) ~= "string" then error("str.replace: third argument must be a string", 2) end + if old == "" then error("str.replace: cannot replace empty string", 2) end + return s:gsub(old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), new) end function str.replace_n(s, old, new, n) - validate_string(s, "str.replace_n", 1) - validate_string(old, "str.replace_n", 2) - validate_string(new, "str.replace_n", 3) - validate_number(n, "str.replace_n", 4) - if n < 0 or n ~= math.floor(n) then - error("str.replace_n: count must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.replace_n: first argument must be a string", 2) end + if type(old) ~= "string" then error("str.replace_n: second argument must be a string", 2) end + if type(new) ~= "string" then error("str.replace_n: third argument must be a string", 2) end + if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then + error("str.replace_n: fourth argument must be a non-negative integer", 2) end - return safe_call(moonshark.string_replace_n, s, old, new, n) + if old == "" then error("str.replace_n: cannot replace empty string", 2) end + local escaped = old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") + return (s:gsub(escaped, new, n)) end function str.index(s, substr) - validate_string(s, "str.index", 1) - validate_string(substr, "str.index", 2) - local idx = safe_call(moonshark.string_index, s, substr) - return idx > 0 and idx or nil + if type(s) ~= "string" then error("str.index: first argument must be a string", 2) end + if type(substr) ~= "string" then error("str.index: second argument must be a string", 2) end + local pos = s:find(substr, 1, true) + return pos end function str.last_index(s, substr) - validate_string(s, "str.last_index", 1) - validate_string(substr, "str.last_index", 2) - local idx = safe_call(moonshark.string_last_index, s, substr) - return idx > 0 and idx or nil + if type(s) ~= "string" then error("str.last_index: first argument must be a string", 2) end + if type(substr) ~= "string" then error("str.last_index: second argument must be a string", 2) end + local last_pos = nil + local pos = 1 + while true do + local found = s:find(substr, pos, true) + if not found then break end + last_pos = found + pos = found + 1 + end + return last_pos end function str.count(s, substr) - validate_string(s, "str.count", 1) - validate_string(substr, "str.count", 2) - return safe_call(moonshark.string_count, s, substr) + if type(s) ~= "string" then error("str.count: first argument must be a string", 2) end + if type(substr) ~= "string" then error("str.count: second argument must be a string", 2) end + if substr == "" then return #s + 1 end + local count = 0 + local pos = 1 + while true do + local found = s:find(substr, pos, true) + if not found then break end + count = count + 1 + pos = found + #substr + end + return count end function str.repeat_(s, n) - validate_string(s, "str.repeat_", 1) - validate_number(n, "str.repeat_", 2) - if n < 0 or n ~= math.floor(n) then - error("str.repeat_: count must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.repeat_: first argument must be a string", 2) end + if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then + error("str.repeat_: second argument must be a non-negative integer", 2) end - return safe_call(moonshark.string_repeat, s, n) + return string.rep(s, n) end function str.reverse(s) - validate_string(s, "str.reverse") - return safe_call(moonshark.string_reverse, s) + if type(s) ~= "string" then error("str.reverse: argument must be a string", 2) end + local result, err = moonshark.string_reverse(s) + if not result then error("str.reverse: " .. err, 2) end + return result end function str.length(s) - validate_string(s, "str.length") - return safe_call(moonshark.string_length, s) + if type(s) ~= "string" then error("str.length: argument must be a string", 2) end + return moonshark.string_length(s) end function str.byte_length(s) - validate_string(s, "str.byte_length") - return safe_call(moonshark.string_byte_length, s) + if type(s) ~= "string" then error("str.byte_length: argument must be a string", 2) end + return #s end function str.lines(s) - validate_string(s, "str.lines") - return safe_call(moonshark.string_lines, s) + if type(s) ~= "string" then error("str.lines: argument must be a string", 2) end + s = s:gsub("\r\n", "\n"):gsub("\r", "\n") + local lines = {} + for line in (s .. "\n"):gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + if #lines > 0 and lines[#lines] == "" then + table.remove(lines) + end + return lines end function str.words(s) - validate_string(s, "str.words") - return safe_call(moonshark.string_words, s) + if type(s) ~= "string" then error("str.words: argument must be a string", 2) end + local words = {} + for word in s:gmatch("%S+") do + table.insert(words, word) + end + return words end function str.pad_left(s, width, pad_char) - validate_string(s, "str.pad_left", 1) - validate_number(width, "str.pad_left", 2) - if width < 0 or width ~= math.floor(width) then - error("str.pad_left: width must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.pad_left: first argument must be a string", 2) end + if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then + error("str.pad_left: second argument must be a non-negative integer", 2) end - if pad_char ~= nil then - validate_string(pad_char, "str.pad_left", 3) - end - return safe_call(moonshark.string_pad_left, s, width, pad_char) + pad_char = pad_char or " " + if type(pad_char) ~= "string" then error("str.pad_left: third argument must be a string", 2) end + if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end + local current_len = str.length(s) + if current_len >= width then return s end + return string.rep(pad_char, width - current_len) .. s end function str.pad_right(s, width, pad_char) - validate_string(s, "str.pad_right", 1) - validate_number(width, "str.pad_right", 2) - if width < 0 or width ~= math.floor(width) then - error("str.pad_right: width must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.pad_right: first argument must be a string", 2) end + if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then + error("str.pad_right: second argument must be a non-negative integer", 2) end - if pad_char ~= nil then - validate_string(pad_char, "str.pad_right", 3) - end - return safe_call(moonshark.string_pad_right, s, width, pad_char) + pad_char = pad_char or " " + if type(pad_char) ~= "string" then error("str.pad_right: third argument must be a string", 2) end + if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end + local current_len = str.length(s) + if current_len >= width then return s end + return s .. string.rep(pad_char, width - current_len) end function str.slice(s, start, end_pos) - validate_string(s, "str.slice", 1) - validate_number(start, "str.slice", 2) - if start ~= math.floor(start) then - error("str.slice: start must be an integer", 2) + if type(s) ~= "string" then error("str.slice: first argument must be a string", 2) end + if type(start) ~= "number" or start ~= math.floor(start) then + error("str.slice: second argument must be an integer", 2) end - if end_pos ~= nil then - validate_number(end_pos, "str.slice", 3) - if end_pos ~= math.floor(end_pos) then - error("str.slice: end position must be an integer", 2) - end + if end_pos ~= nil and (type(end_pos) ~= "number" or end_pos ~= math.floor(end_pos)) then + error("str.slice: third argument must be an integer", 2) end - return safe_call(moonshark.string_slice, s, start, end_pos) + local result, err = moonshark.string_slice(s, start, end_pos) + if not result then error("str.slice: " .. err, 2) end + return result end -- ====================================================================== --- REGULAR EXPRESSIONS +-- REGULAR EXPRESSIONS (Go Functions) -- ====================================================================== function str.match(pattern, s) - validate_string(pattern, "str.match", 1) - validate_string(s, "str.match", 2) - return safe_call(moonshark.regex_match, pattern, s) + if type(pattern) ~= "string" then error("str.match: first argument must be a string", 2) end + if type(s) ~= "string" then error("str.match: second argument must be a string", 2) end + return moonshark.regex_match(pattern, s) end function str.find(pattern, s) - validate_string(pattern, "str.find", 1) - validate_string(s, "str.find", 2) - return safe_call(moonshark.regex_find, pattern, s) + if type(pattern) ~= "string" then error("str.find: first argument must be a string", 2) end + if type(s) ~= "string" then error("str.find: second argument must be a string", 2) end + return moonshark.regex_find(pattern, s) end function str.find_all(pattern, s) - validate_string(pattern, "str.find_all", 1) - validate_string(s, "str.find_all", 2) - return safe_call(moonshark.regex_find_all, pattern, s) + if type(pattern) ~= "string" then error("str.find_all: first argument must be a string", 2) end + if type(s) ~= "string" then error("str.find_all: second argument must be a string", 2) end + return moonshark.regex_find_all(pattern, s) end function str.gsub(pattern, s, replacement) - validate_string(pattern, "str.gsub", 1) - validate_string(s, "str.gsub", 2) - validate_string(replacement, "str.gsub", 3) - return safe_call(moonshark.regex_replace, pattern, s, replacement) + if type(pattern) ~= "string" then error("str.gsub: first argument must be a string", 2) end + if type(s) ~= "string" then error("str.gsub: second argument must be a string", 2) end + if type(replacement) ~= "string" then error("str.gsub: third argument must be a string", 2) end + return moonshark.regex_replace(pattern, s, replacement) end -- ====================================================================== @@ -248,23 +256,27 @@ end -- ====================================================================== function str.to_number(s) - validate_string(s, "str.to_number") - return safe_call(moonshark.string_to_number, s) + if type(s) ~= "string" then error("str.to_number: argument must be a string", 2) end + s = str.trim(s) + return tonumber(s) end function str.is_numeric(s) - validate_string(s, "str.is_numeric") - return safe_call(moonshark.string_is_numeric, s) + if type(s) ~= "string" then error("str.is_numeric: argument must be a string", 2) end + s = str.trim(s) + return tonumber(s) ~= nil end function str.is_alpha(s) - validate_string(s, "str.is_alpha") - return safe_call(moonshark.string_is_alpha, s) + if type(s) ~= "string" then error("str.is_alpha: argument must be a string", 2) end + if #s == 0 then return false end + return s:match("^%a+$") ~= nil end function str.is_alphanumeric(s) - validate_string(s, "str.is_alphanumeric") - return safe_call(moonshark.string_is_alphanumeric, s) + if type(s) ~= "string" then error("str.is_alphanumeric: argument must be a string", 2) end + if #s == 0 then return false end + return s:match("^%w+$") ~= nil end function str.is_empty(s) @@ -276,105 +288,74 @@ function str.is_blank(s) end function str.is_utf8(s) - validate_string(s, "str.is_utf8") - return safe_call(moonshark.string_is_valid_utf8, s) -end - -function str.is_valid_utf8(s) - validate_string(s, "str.is_valid_utf8") - return safe_call(moonshark.string_is_valid_utf8, s) + if type(s) ~= "string" then error("str.is_utf8: argument must be a string", 2) end + return moonshark.string_is_valid_utf8(s) end -- ====================================================================== --- ADVANCED STRING OPERATIONS +-- ADVANCED STRING OPERATIONS (Pure Lua) -- ====================================================================== --- Capitalize first letter of each word (Pure Lua - faster) function str.capitalize(s) - validate_string(s, "str.capitalize") + if type(s) ~= "string" then error("str.capitalize: argument must be a string", 2) end return s:gsub("(%a)([%w_']*)", function(first, rest) return first:upper() .. rest:lower() end) end --- Convert string to camelCase (Pure Lua - faster) function str.camel_case(s) - validate_string(s, "str.camel_case") - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word:lower()) - end + if type(s) ~= "string" then error("str.camel_case: argument must be a string", 2) end + local words = str.words(s) if #words == 0 then return s end - - local result = words[1] + local result = words[1]:lower() for i = 2, #words do - result = result .. words[i]:gsub("^%l", string.upper) + result = result .. words[i]:sub(1,1):upper() .. words[i]:sub(2):lower() end return result end --- Convert string to PascalCase (Pure Lua - faster) function str.pascal_case(s) - validate_string(s, "str.pascal_case") - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word:lower()) - end + if type(s) ~= "string" then error("str.pascal_case: argument must be a string", 2) end + local words = str.words(s) local result = "" for _, word in ipairs(words) do - result = result .. word:gsub("^%l", string.upper) + result = result .. word:sub(1,1):upper() .. word:sub(2):lower() end return result end --- Convert string to snake_case (Pure Lua - faster) function str.snake_case(s) - validate_string(s, "str.snake_case") - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word:lower()) + if type(s) ~= "string" then error("str.snake_case: argument must be a string", 2) end + local words = str.words(s) + local result = {} + for _, word in ipairs(words) do + table.insert(result, word:lower()) end - return table.concat(words, "_") + return table.concat(result, "_") end --- Convert string to kebab-case (Pure Lua - faster) function str.kebab_case(s) - validate_string(s, "str.kebab_case") - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word:lower()) + if type(s) ~= "string" then error("str.kebab_case: argument must be a string", 2) end + local words = str.words(s) + local result = {} + for _, word in ipairs(words) do + table.insert(result, word:lower()) end - return table.concat(words, "-") + return table.concat(result, "-") end --- Convert string to SCREAMING_SNAKE_CASE (Pure Lua - faster) -function str.screaming_snake_case(s) - validate_string(s, "str.screaming_snake_case") - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word:upper()) - end - return table.concat(words, "_") -end - --- Center text within given width (Pure Lua - faster) function str.center(s, width, fill_char) - validate_string(s, "str.center", 1) - validate_number(width, "str.center", 2) - if width < 0 or width ~= math.floor(width) then - error("str.center: width must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.center: first argument must be a string", 2) end + if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then + error("str.center: second argument must be a non-negative integer", 2) end - fill_char = fill_char or " " - if fill_char ~= nil then - validate_string(fill_char, "str.center", 3) - if #fill_char == 0 then - error("str.center: fill character cannot be empty", 2) - end - fill_char = fill_char:sub(1,1) -- Use only first character + if type(fill_char) ~= "string" or #fill_char == 0 then + error("str.center: fill character must be a non-empty string", 2) end + fill_char = fill_char:sub(1,1) - local len = #s + local len = str.length(s) if len >= width then return s end local pad_total = width - len @@ -384,192 +365,68 @@ function str.center(s, width, fill_char) return string.rep(fill_char, pad_left) .. s .. string.rep(fill_char, pad_right) end --- Truncate string to maximum length (Pure Lua - faster) function str.truncate(s, max_length, suffix) - validate_string(s, "str.truncate", 1) - validate_number(max_length, "str.truncate", 2) - if max_length < 0 or max_length ~= math.floor(max_length) then - error("str.truncate: max_length must be a non-negative integer", 2) + if type(s) ~= "string" then error("str.truncate: first argument must be a string", 2) end + if type(max_length) ~= "number" or max_length < 0 or max_length ~= math.floor(max_length) then + error("str.truncate: second argument must be a non-negative integer", 2) end - suffix = suffix or "..." - validate_string(suffix, "str.truncate", 3) + if type(suffix) ~= "string" then error("str.truncate: third argument must be a string", 2) end - if #s <= max_length then - return s - end + local len = str.length(s) + if len <= max_length then return s end - local suffix_len = #suffix + local suffix_len = str.length(suffix) if max_length <= suffix_len then - return suffix:sub(1, max_length) + return str.slice(suffix, 1, max_length) end - local main_part = s:sub(1, max_length - suffix_len) - main_part = main_part:gsub("%s+$", "") -- trim right + local main_part = str.slice(s, 1, max_length - suffix_len) + main_part = str.trim_right(main_part) return main_part .. suffix end --- Wrap text to specified width (Pure Lua - much faster) -function str.wrap(s, width) - validate_string(s, "str.wrap", 1) - validate_number(width, "str.wrap", 2) - if width <= 0 or width ~= math.floor(width) then - error("str.wrap: width must be a positive integer", 2) - end - - local words = {} - for word in s:gmatch("%S+") do - table.insert(words, word) - end - - local lines = {} - local current_line = "" - - for _, word in ipairs(words) do - local word_len = #word - local current_len = #current_line - - -- Handle words longer than width - if word_len > width then - if current_line ~= "" then - table.insert(lines, current_line) - current_line = "" - end - -- Break long word - while #word > width do - table.insert(lines, word:sub(1, width)) - word = word:sub(width + 1) - end - if #word > 0 then - current_line = word - end - elseif current_len + word_len + 1 <= width then - if current_line == "" then - current_line = word - else - current_line = current_line .. " " .. word - end - else - if current_line ~= "" then - table.insert(lines, current_line) - end - current_line = word - end - end - - if current_line ~= "" then - table.insert(lines, current_line) - end - - return lines -end - --- Remove common leading whitespace (Pure Lua - faster) -function str.dedent(s) - validate_string(s, "str.dedent") - local lines = {} - for line in (s.."\n"):gmatch("([^\n]*)\n") do - table.insert(lines, line) - end - if #lines <= 1 then return s end - - -- Find minimum indentation (excluding empty lines) - local min_indent = math.huge - for _, line in ipairs(lines) do - local trimmed = line:gsub("%s", "") - if trimmed ~= "" then - local indent = line:match("^%s*") - if indent then - min_indent = math.min(min_indent, #indent) - end - end - end - - if min_indent == math.huge or min_indent == 0 then - return s - end - - -- Remove common indentation - for i, line in ipairs(lines) do - local trimmed = line:gsub("%s", "") - if trimmed ~= "" then - lines[i] = line:sub(min_indent + 1) - end - end - - return table.concat(lines, "\n") -end - --- Escape special characters for regex function str.escape_regex(s) - validate_string(s, "str.escape_regex") + if type(s) ~= "string" then error("str.escape_regex: argument must be a string", 2) end return s:gsub("([%.%+%*%?%[%]%^%$%(%)%{%}%|%\\])", "\\%1") end --- Quote string for shell usage -function str.shell_quote(s) - validate_string(s, "str.shell_quote") - return "'" .. s:gsub("'", "'\"'\"'") .. "'" -end - --- URL encode string function str.url_encode(s) - validate_string(s, "str.url_encode") + if type(s) ~= "string" then error("str.url_encode: argument must be a string", 2) end return s:gsub("([^%w%-%.%_%~])", function(c) return string.format("%%%02X", string.byte(c)) end) end --- URL decode string function str.url_decode(s) - validate_string(s, "str.url_decode") + if type(s) ~= "string" then error("str.url_decode: argument must be a string", 2) end local result = s:gsub("%%(%x%x)", function(hex) local byte = tonumber(hex, 16) - if byte then - return string.char(byte) - else - return "%" .. hex -- Invalid hex, keep original - end + return byte and string.char(byte) or ("%" .. hex) end):gsub("+", " ") - -- Validate result is UTF-8 - if not str.is_valid_utf8(result) then + if not str.is_utf8(result) then error("str.url_decode: result is not valid UTF-8", 2) end return result end --- ====================================================================== --- STRING COMPARISON --- ====================================================================== - --- Case-insensitive comparison -function str.iequals(a, b) - validate_string(a, "str.iequals", 1) - validate_string(b, "str.iequals", 2) - return str.lower(a) == str.lower(b) -end - --- Levenshtein distance (Pure Lua - much faster) function str.distance(a, b) - validate_string(a, "str.distance", 1) - validate_string(b, "str.distance", 2) + if type(a) ~= "string" then error("str.distance: first argument must be a string", 2) end + if type(b) ~= "string" then error("str.distance: second argument must be a string", 2) end - local len_a, len_b = #a, #b + local len_a, len_b = str.length(a), str.length(b) - -- Handle empty strings if len_a == 0 then return len_b end if len_b == 0 then return len_a end - -- Limit computation for very long strings if len_a > 1000 or len_b > 1000 then error("str.distance: strings too long for distance calculation", 2) end local matrix = {} - -- Initialize matrix for i = 0, len_a do matrix[i] = {[0] = i} end @@ -577,14 +434,13 @@ function str.distance(a, b) matrix[0][j] = j end - -- Fill matrix for i = 1, len_a do for j = 1, len_b do - local cost = (a:sub(i,i) == b:sub(j,j)) and 0 or 1 + local cost = (str.slice(a, i, i) == str.slice(b, j, j)) and 0 or 1 matrix[i][j] = math.min( - matrix[i-1][j] + 1, -- deletion - matrix[i][j-1] + 1, -- insertion - matrix[i-1][j-1] + cost -- substitution + matrix[i-1][j] + 1, + matrix[i][j-1] + 1, + matrix[i-1][j-1] + cost ) end end @@ -592,173 +448,44 @@ function str.distance(a, b) return matrix[len_a][len_b] end --- String similarity (0-1) (Pure Lua - faster) function str.similarity(a, b) - validate_string(a, "str.similarity", 1) - validate_string(b, "str.similarity", 2) + if type(a) ~= "string" then error("str.similarity: first argument must be a string", 2) end + if type(b) ~= "string" then error("str.similarity: second argument must be a string", 2) end - local max_len = math.max(#a, #b) + local max_len = math.max(str.length(a), str.length(b)) if max_len == 0 then return 1.0 end local dist = str.distance(a, b) return 1.0 - (dist / max_len) end --- ====================================================================== --- TEMPLATE FUNCTIONS --- ====================================================================== - --- Simple template substitution (Pure Lua - faster) function str.template(template, vars) - validate_string(template, "str.template", 1) - if vars ~= nil then - validate_table(vars, "str.template", 2) - else - vars = {} - end + if type(template) ~= "string" then error("str.template: first argument must be a string", 2) end + vars = vars or {} + if type(vars) ~= "table" then error("str.template: second argument must be a table", 2) end return template:gsub("%${([%w_]+)}", function(var) local value = vars[var] - if value == nil then - return "" - else - return tostring(value) - end + return value ~= nil and tostring(value) or "" end) end --- Advanced template with functions (Pure Lua - faster) -function str.template_advanced(template, context) - validate_string(template, "str.template_advanced", 1) - if context ~= nil then - validate_table(context, "str.template_advanced", 2) - else - context = {} +function str.random(length, charset) + if type(length) ~= "number" or length < 0 or length ~= math.floor(length) then + error("str.random: first argument must be a non-negative integer", 2) end - - return template:gsub("%${([^}]+)}", function(expr) - -- Simple variable substitution - if context[expr] then - return tostring(context[expr]) - end - - -- Handle simple expressions like ${var.prop} - local parts = {} - for part in expr:gmatch("[^%.]+") do - table.insert(parts, part) - end - - local value = context - for _, part in ipairs(parts) do - if type(value) == "table" and value[part] ~= nil then - value = value[part] - else - return "" - end - end - - return tostring(value) - end) -end - --- ====================================================================== --- UTILITY FUNCTIONS --- ====================================================================== - --- Check if string contains only whitespace -function str.is_whitespace(s) - validate_string(s, "str.is_whitespace") - return s:match("^%s*$") ~= nil -end - --- Remove all whitespace -function str.strip_whitespace(s) - validate_string(s, "str.strip_whitespace") - return s:gsub("%s", "") -end - --- Normalize whitespace (replace multiple spaces with single space) -function str.normalize_whitespace(s) - validate_string(s, "str.normalize_whitespace") - return str.trim(s:gsub("%s+", " ")) -end - --- Extract numbers from string -function str.extract_numbers(s) - validate_string(s, "str.extract_numbers") - local numbers = {} - for num in s:gmatch("%-?%d+%.?%d*") do - local n = tonumber(num) - if n then table.insert(numbers, n) end - end - return numbers -end - --- Remove diacritics/accents -function str.remove_accents(s) - validate_string(s, "str.remove_accents") - local accents = { - ["à"] = "a", ["á"] = "a", ["â"] = "a", ["ã"] = "a", ["ä"] = "a", ["å"] = "a", - ["è"] = "e", ["é"] = "e", ["ê"] = "e", ["ë"] = "e", - ["ì"] = "i", ["í"] = "i", ["î"] = "i", ["ï"] = "i", - ["ò"] = "o", ["ó"] = "o", ["ô"] = "o", ["õ"] = "o", ["ö"] = "o", - ["ù"] = "u", ["ú"] = "u", ["û"] = "u", ["ü"] = "u", - ["ñ"] = "n", ["ç"] = "c", ["ý"] = "y", ["ÿ"] = "y", - -- Uppercase versions - ["À"] = "A", ["Á"] = "A", ["Â"] = "A", ["Ã"] = "A", ["Ä"] = "A", ["Å"] = "A", - ["È"] = "E", ["É"] = "E", ["Ê"] = "E", ["Ë"] = "E", - ["Ì"] = "I", ["Í"] = "I", ["Î"] = "I", ["Ï"] = "I", - ["Ò"] = "O", ["Ó"] = "O", ["Ô"] = "O", ["Õ"] = "O", ["Ö"] = "O", - ["Ù"] = "U", ["Ú"] = "U", ["Û"] = "U", ["Ü"] = "U", - ["Ñ"] = "N", ["Ç"] = "C", ["Ý"] = "Y", ["Ÿ"] = "Y" - } - - local result = s - for accented, plain in pairs(accents) do - result = result:gsub(accented, plain) + if charset ~= nil and type(charset) ~= "string" then + error("str.random: second argument must be a string", 2) end + local result, err = moonshark.random_string(length, charset) + if not result then error("str.random: " .. err, 2) end return result end --- Generate random string -function str.random(length, charset) - validate_number(length, "str.random", 1) - if length < 0 or length ~= math.floor(length) then - error("str.random: length must be a non-negative integer", 2) - end - if charset ~= nil then - validate_string(charset, "str.random", 2) - end - return safe_call(moonshark.random_string, length, charset) -end - --- Generate slug from string (Pure Lua - faster) function str.slug(s) - validate_string(s, "str.slug") - - -- Remove accents (simplified but faster) - local accents = { - ["à"] = "a", ["á"] = "a", ["â"] = "a", ["ã"] = "a", ["ä"] = "a", ["å"] = "a", - ["è"] = "e", ["é"] = "e", ["ê"] = "e", ["ë"] = "e", - ["ì"] = "i", ["í"] = "i", ["î"] = "i", ["ï"] = "i", - ["ò"] = "o", ["ó"] = "o", ["ô"] = "o", ["õ"] = "o", ["ö"] = "o", - ["ù"] = "u", ["ú"] = "u", ["û"] = "u", ["ü"] = "u", - ["ñ"] = "n", ["ç"] = "c", ["ý"] = "y", ["ÿ"] = "y", - -- Uppercase versions - ["À"] = "A", ["Á"] = "A", ["Â"] = "A", ["Ã"] = "A", ["Ä"] = "A", ["Å"] = "A", - ["È"] = "E", ["É"] = "E", ["Ê"] = "E", ["Ë"] = "E", - ["Ì"] = "I", ["Í"] = "I", ["Î"] = "I", ["Ï"] = "I", - ["Ò"] = "O", ["Ó"] = "O", ["Ô"] = "O", ["Õ"] = "O", ["Ö"] = "O", - ["Ù"] = "U", ["Ú"] = "U", ["Û"] = "U", ["Ü"] = "U", - ["Ñ"] = "N", ["Ç"] = "C", ["Ý"] = "Y", ["Ÿ"] = "Y" - } + if type(s) ~= "string" then error("str.slug: argument must be a string", 2) end local result = s:lower() - for accented, plain in pairs(accents) do - result = result:gsub(accented:lower(), plain:lower()) - end - - -- Keep only alphanumeric characters and spaces, then convert spaces to hyphens result = result:gsub("[^%w%s]", "") result = result:gsub("%s+", "-") result = result:gsub("^%-+", ""):gsub("%-+$", "")