package processor import ( "fmt" "log" "os" "path/filepath" "strings" "github.com/antchfx/xmlquery" lua "github.com/yuin/gopher-lua" ) // Processor defines the interface for all file processors type Processor interface { // Process handles processing a file with the given pattern and Lua expression // Now implemented as a base function in processor.go // Process(filename string, pattern string, luaExpr string) (int, int, error) // ProcessContent handles processing a string content directly with the given pattern and Lua expression // Returns the modified content, modification count, match count, and any error ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) // ToLua converts processor-specific data to Lua variables ToLua(L *lua.LState, data interface{}) error // FromLua retrieves modified data from Lua FromLua(L *lua.LState) (interface{}, error) } // ModificationRecord tracks a single value modification type ModificationRecord struct { File string OldValue string NewValue string Operation string Context string } func NewLuaState() (*lua.LState, error) { L := lua.NewState() // defer L.Close() // Load math library L.Push(L.GetGlobal("require")) L.Push(lua.LString("math")) if err := L.PCall(1, 1, nil); err != nil { return nil, fmt.Errorf("error loading Lua math library: %v", err) } // Initialize helper functions if err := InitLuaHelpers(L); err != nil { return nil, err } return L, nil } func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) { // Read file content cwd, err := os.Getwd() if err != nil { return 0, 0, fmt.Errorf("error getting current working directory: %v", err) } fullPath := filepath.Join(cwd, filename) content, err := os.ReadFile(fullPath) if err != nil { return 0, 0, fmt.Errorf("error reading file: %v", err) } fileContent := string(content) // Process the content modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr) if err != nil { return 0, 0, err } // If we made modifications, save the file if modCount > 0 { err = os.WriteFile(fullPath, []byte(modifiedContent), 0644) if err != nil { return 0, 0, fmt.Errorf("error writing file: %v", err) } } return modCount, matchCount, nil } // ToLua converts a struct or map to a Lua table recursively func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) { switch v := data.(type) { case *xmlquery.Node: luaTable := L.NewTable() luaTable.RawSetString("text", lua.LString(v.Data)) // Should be a map, simple key value pairs attr, err := ToLua(L, v.Attr) if err != nil { return nil, err } luaTable.RawSetString("attr", attr) return luaTable, nil case map[string]interface{}: luaTable := L.NewTable() for key, value := range v { luaValue, err := ToLua(L, value) if err != nil { return nil, err } luaTable.RawSetString(key, luaValue) } return luaTable, nil case []interface{}: luaTable := L.NewTable() for i, value := range v { luaValue, err := ToLua(L, value) if err != nil { return nil, err } luaTable.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed } return luaTable, nil case string: return lua.LString(v), nil case bool: return lua.LBool(v), nil case float64: return lua.LNumber(v), nil case nil: return lua.LNil, nil default: return nil, fmt.Errorf("unsupported data type: %T", data) } } // FromLua converts a Lua table to a struct or map recursively func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) { switch v := luaValue.(type) { // Well shit... // Tables in lua are both maps and arrays // As arrays they are ordered and as maps, obviously, not // So when we parse them to a go map we fuck up the order for arrays // We have to find a better way.... case *lua.LTable: isArray, err := IsLuaTableArray(L, v) if err != nil { return nil, err } if isArray { result := make([]interface{}, 0) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result = append(result, converted) }) return result, nil } else { result := make(map[string]interface{}) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result[key.String()] = converted }) return result, nil } case lua.LString: return string(v), nil case lua.LBool: return bool(v), nil case lua.LNumber: return float64(v), nil default: return nil, nil } } func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) { L.SetGlobal("table_to_check", v) // Use our predefined helper function from InitLuaHelpers err := L.DoString(`is_array = isArray(table_to_check)`) if err != nil { return false, fmt.Errorf("error determining if table is array: %w", err) } // Check the result of our Lua function isArray := L.GetGlobal("is_array") // LVIsFalse returns true if a given LValue is a nil or false otherwise false. if !lua.LVIsFalse(isArray) { return true, nil } return false, nil } // InitLuaHelpers initializes common Lua helper functions func InitLuaHelpers(L *lua.LState) error { helperScript := ` -- Custom Lua helpers for math operations function min(a, b) return math.min(a, b) end function max(a, b) return math.max(a, b) end function round(x, n) if n == nil then n = 0 end return math.floor(x * 10^n + 0.5) / 10^n end function floor(x) return math.floor(x) end function ceil(x) return math.ceil(x) end function upper(s) return string.upper(s) end function lower(s) return string.lower(s) end -- String to number conversion helper function num(str) return tonumber(str) or 0 end -- Number to string conversion function str(num) return tostring(num) end -- Check if string is numeric function is_number(str) return tonumber(str) ~= nil end function isArray(t) if type(t) ~= "table" then return false end local max = 0 local count = 0 for k, _ in pairs(t) do if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then return false end max = math.max(max, k) count = count + 1 end return max == count end modified = false ` if err := L.DoString(helperScript); err != nil { return fmt.Errorf("error loading helper functions: %v", err) } L.SetGlobal("print", L.NewFunction(printToGo)) return nil } // Helper utility functions // LimitString truncates a string to maxLen and adds "..." if truncated func LimitString(s string, maxLen int) string { s = strings.ReplaceAll(s, "\n", "\\n") if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." } func PrependLuaAssignment(luaExpr string) string { // Auto-prepend v1 for expressions starting with operators if strings.HasPrefix(luaExpr, "*") || strings.HasPrefix(luaExpr, "/") || strings.HasPrefix(luaExpr, "+") || strings.HasPrefix(luaExpr, "-") || strings.HasPrefix(luaExpr, "^") || strings.HasPrefix(luaExpr, "%") { luaExpr = "v1 = v1" + luaExpr } else if strings.HasPrefix(luaExpr, "=") { // Handle direct assignment with = operator luaExpr = "v1 " + luaExpr } // Add assignment if needed if !strings.Contains(luaExpr, "=") { luaExpr = "v1 = " + luaExpr } return luaExpr } // BuildLuaScript prepares a Lua expression from shorthand notation func BuildLuaScript(luaExpr string) string { luaExpr = PrependLuaAssignment(luaExpr) // This allows the user to specify whether or not they modified a value // If they do nothing we assume they did modify (no return at all) // If they return before our return then they themselves specify what they did // If nothing is returned lua assumes nil // So we can say our value was modified if the return value is either nil or true // If the return value is false then the user wants to keep the original fullScript := fmt.Sprintf(` function run() %s end local res = run() modified = res == nil or res `, luaExpr) return fullScript } func printToGo(L *lua.LState) int { // Get the number of arguments passed to the Lua print function n := L.GetTop() // Create a slice to hold the arguments args := make([]interface{}, n) for i := 1; i <= n; i++ { args[i-1] = L.Get(i) // Get the argument from Lua stack } // Print the arguments to Go's stdout log.Print("Lua: ") log.Println(args...) return 0 // No return values } // Max returns the maximum of two integers func Max(a, b int) int { if a > b { return a } return b } // Min returns the minimum of two integers func Min(a, b int) int { if a < b { return a } return b }