diff --git a/modules/crypto/crypto.go b/modules/crypto/crypto.go index d4877e1..75189f4 100644 --- a/modules/crypto/crypto.go +++ b/modules/crypto/crypto.go @@ -204,10 +204,16 @@ func random_string(s *luajit.State) int { } charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - if s.GetTop() >= 2 { + if s.GetTop() >= 2 && !s.IsNil(2) { charset = s.ToString(2) } + if len(charset) == 0 { + s.PushNil() + s.PushString("empty charset") + return 2 + } + result := make([]byte, length) charsetLen := big.NewInt(int64(len(charset))) for i := range result { diff --git a/modules/registry.go b/modules/registry.go index 46644ae..5fb9a47 100644 --- a/modules/registry.go +++ b/modules/registry.go @@ -1,4 +1,4 @@ -package registry +package modules import ( "embed" @@ -8,7 +8,11 @@ import ( "strings" "sync" - "Moonshark/functions" + "Moonshark/modules/crypto" + "Moonshark/modules/fs" + "Moonshark/modules/json" + "Moonshark/modules/math" + lua_string "Moonshark/modules/string" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) @@ -16,7 +20,7 @@ import ( // Global registry instance var Global *Registry -//go:embed modules/*.lua +//go:embed crypto/*.lua fs/*.lua json/*.lua math/*.lua string/*.lua var embeddedModules embed.FS // Registry manages all Lua modules and Go functions @@ -33,33 +37,56 @@ func New() *Registry { goFuncs: make(map[string]luajit.GoFunction), } - // Load all Go functions - maps.Copy(r.goFuncs, functions.GetJSONFunctions()) - maps.Copy(r.goFuncs, functions.GetStringFunctions()) - maps.Copy(r.goFuncs, functions.GetMathFunctions()) - maps.Copy(r.goFuncs, functions.GetFSFunctions()) - maps.Copy(r.goFuncs, functions.GetCryptoFunctions()) + // Load all Go functions from each module + maps.Copy(r.goFuncs, json.GetFunctionList()) + maps.Copy(r.goFuncs, lua_string.GetFunctionList()) + maps.Copy(r.goFuncs, math.GetFunctionList()) + maps.Copy(r.goFuncs, fs.GetFunctionList()) + maps.Copy(r.goFuncs, crypto.GetFunctionList()) return r } // LoadEmbeddedModules loads all .lua files from embedded filesystem func (r *Registry) LoadEmbeddedModules() error { - entries, err := embeddedModules.ReadDir("modules") - if err != nil { - return fmt.Errorf("failed to read modules directory: %w", err) - } - r.mutex.Lock() defer r.mutex.Unlock() + // Load modules from subdirectories + subdirs := []string{"crypto", "fs", "json", "math", "string"} + for _, subdir := range subdirs { + if err := r.loadModulesFromDir(subdir); err != nil { + return err + } + } + + return nil +} + +// loadModulesFromDir loads all .lua files from a specific directory +func (r *Registry) loadModulesFromDir(dir string) error { + entries, err := embeddedModules.ReadDir(dir) + if err != nil { + return nil // Skip missing directories + } + for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".lua") { continue } - moduleName := strings.TrimSuffix(entry.Name(), ".lua") - source, err := embeddedModules.ReadFile(filepath.Join("modules", entry.Name())) + fileName := strings.TrimSuffix(entry.Name(), ".lua") + + // If filename matches directory name (e.g., math/math.lua), + // register as just the directory name for require("math") + var moduleName string + if fileName == dir { + moduleName = dir + } else { + moduleName = dir + "/" + fileName + } + + source, err := embeddedModules.ReadFile(filepath.Join(dir, entry.Name())) if err != nil { return fmt.Errorf("failed to read module %s: %w", moduleName, err) } @@ -138,15 +165,3 @@ func Initialize() error { Global = New() return Global.LoadEmbeddedModules() } - -// GetGoFunctions returns all Go functions -func (r *Registry) GetGoFunctions() map[string]luajit.GoFunction { - r.mutex.RLock() - defer r.mutex.RUnlock() - - result := make(map[string]luajit.GoFunction, len(r.goFuncs)) - for k, v := range r.goFuncs { - result[k] = v - } - return result -} diff --git a/modules/string/string.go b/modules/string/string.go index 68ab992..31b26f2 100644 --- a/modules/string/string.go +++ b/modules/string/string.go @@ -80,6 +80,15 @@ func string_join(s *luajit.State) int { parts[i] = s.ToString(-1) // Convert via Lua } } + case map[string]any: + // Empty table {} from Lua becomes map[string]any{} + if len(v) == 0 { + parts = []string{} // Empty array + } else { + s.PushNil() + s.PushString("not an array") + return 2 + } default: s.PushNil() s.PushString("not an array") diff --git a/modules/string/string.lua b/modules/string/string.lua index 22c178a..fd9f980 100644 --- a/modules/string/string.lua +++ b/modules/string/string.lua @@ -163,6 +163,8 @@ end function str.lines(s) if type(s) ~= "string" then error("str.lines: argument must be a string", 2) end + if s == "" then return {""} end + s = s:gsub("\r\n", "\n"):gsub("\r", "\n") local lines = {} for line in (s .. "\n"):gmatch("([^\n]*)\n") do @@ -485,7 +487,7 @@ end function str.slug(s) if type(s) ~= "string" then error("str.slug: argument must be a string", 2) end - local result = s:lower() + local result = str.remove_accents(s):lower() result = result:gsub("[^%w%s]", "") result = result:gsub("%s+", "-") result = result:gsub("^%-+", ""):gsub("%-+$", "") @@ -493,4 +495,155 @@ function str.slug(s) return result end +-- Add these functions to the end of string.lua, before the return statement + +function str.screaming_snake_case(s) + if type(s) ~= "string" then error("str.screaming_snake_case: argument must be a string", 2) end + return str.snake_case(s):upper() +end + +function str.wrap(s, width) + if type(s) ~= "string" then error("str.wrap: first argument must be a string", 2) end + if type(width) ~= "number" or width <= 0 then error("str.wrap: width must be positive number", 2) end + + local words = str.words(s) + local lines = {} + local current_line = "" + + for _, word in ipairs(words) do + if current_line == "" then + current_line = word + elseif str.length(current_line .. " " .. word) <= width then + current_line = current_line .. " " .. word + else + table.insert(lines, current_line) + current_line = word + end + end + + if current_line ~= "" then + table.insert(lines, current_line) + end + + return lines +end + +function str.dedent(s) + if type(s) ~= "string" then error("str.dedent: argument must be a string", 2) end + + local lines = str.lines(s) + if #lines == 0 then return "" end + + -- Find minimum indentation + local min_indent = math.huge + for _, line in ipairs(lines) do + if line:match("%S") then -- Non-empty line + local indent = line:match("^(%s*)") + min_indent = math.min(min_indent, #indent) + end + end + + if min_indent == math.huge then min_indent = 0 end + + -- Remove common indentation + local result = {} + for _, line in ipairs(lines) do + table.insert(result, line:sub(min_indent + 1)) + end + + return table.concat(result, "\n") +end + +function str.shell_quote(s) + if type(s) ~= "string" then error("str.shell_quote: argument must be a string", 2) end + + if s:match("^[%w%-%./]+$") then + return s -- No quoting needed + end + + -- Replace single quotes with '"'"' + local quoted = s:gsub("'", "'\"'\"'") + return "'" .. quoted .. "'" +end + +function str.iequals(a, b) + if type(a) ~= "string" then error("str.iequals: first argument must be a string", 2) end + if type(b) ~= "string" then error("str.iequals: second argument must be a string", 2) end + return str.lower(a) == str.lower(b) +end + +function str.template_advanced(template, context) + if type(template) ~= "string" then error("str.template_advanced: first argument must be a string", 2) end + context = context or {} + if type(context) ~= "table" then error("str.template_advanced: second argument must be a table", 2) end + + return template:gsub("%${([%w_.]+)}", function(path) + local keys = str.split(path, ".") + local value = context + + for _, key in ipairs(keys) do + if type(value) == "table" and value[key] ~= nil then + value = value[key] + else + return "" + end + end + + return tostring(value) + end) +end + +function str.is_whitespace(s) + if type(s) ~= "string" then error("str.is_whitespace: argument must be a string", 2) end + return s:match("^%s*$") ~= nil +end + +function str.strip_whitespace(s) + if type(s) ~= "string" then error("str.strip_whitespace: argument must be a string", 2) end + return s:gsub("%s", "") +end + +function str.normalize_whitespace(s) + if type(s) ~= "string" then error("str.normalize_whitespace: argument must be a string", 2) end + return str.trim(s:gsub("%s+", " ")) +end + +function str.extract_numbers(s) + if type(s) ~= "string" then error("str.extract_numbers: argument must be a string", 2) end + + local numbers = {} + for match in s:gmatch("%-?%d+%.?%d*") do + local num = tonumber(match) + if num then + table.insert(numbers, num) + end + end + return numbers +end + +function str.remove_accents(s) + if type(s) ~= "string" then error("str.remove_accents: argument must be a string", 2) end + + 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", + ["À"] = "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" + } + + local result = s + for accented, plain in pairs(accents) do + result = result:gsub(accented, plain) + end + return result +end + return str \ No newline at end of file diff --git a/moonshark.go b/moonshark.go index fd143e7..03c6813 100644 --- a/moonshark.go +++ b/moonshark.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "Moonshark/registry" + "Moonshark/modules" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) @@ -25,7 +25,7 @@ func main() { } // Initialize global registry - if err := registry.Initialize(); err != nil { + if err := modules.Initialize(); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to initialize registry: %v\n", err) os.Exit(1) } @@ -39,7 +39,7 @@ func main() { defer state.Close() // Install module system in main state - if err := registry.Global.InstallInState(state); err != nil { + if err := modules.Global.InstallInState(state); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to install module system: %v\n", err) os.Exit(1) }