Clean up after claude

This commit is contained in:
2025-03-24 16:23:24 +01:00
parent 4f70eaa329
commit 1d39b5287f
10 changed files with 2947 additions and 2160 deletions

4
go.mod
View File

@@ -9,8 +9,12 @@ require (
)
require (
github.com/PaesslerAG/gval v1.0.0 // indirect
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
github.com/antchfx/xpath v1.3.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

21
go.sum
View File

@@ -1,12 +1,29 @@
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
@@ -75,3 +92,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

280
main.go
View File

@@ -3,11 +3,8 @@ package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
"sync"
"github.com/bmatcuk/doublestar/v4"
@@ -15,66 +12,37 @@ import (
"modify/processor"
)
var Error *log.Logger
var Warning *log.Logger
var Info *log.Logger
var Success *log.Logger
// GlobalStats tracks all modifications across files
type GlobalStats struct {
TotalMatches int
TotalModifications int
Modifications []processor.ModificationRecord
ProcessedFiles int
FailedFiles int
}
// FileMode defines how we interpret and process files
type FileMode string
const (
ModeRegex FileMode = "regex" // Default mode using regex
ModeXML FileMode = "xml" // XML mode using XPath
ModeJSON FileMode = "json" // JSON mode using JSONPath
ModeRegex FileMode = "regex"
ModeXML FileMode = "xml"
ModeJSON FileMode = "json"
)
var stats GlobalStats
var logger *log.Logger
var (
fileModeFlag = flag.String("mode", "regex", "Processing mode: regex, xml, json")
verboseFlag = flag.Bool("verbose", false, "Enable verbose output")
)
func init() {
// Configure standard logging to be hidden by default
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
log.SetOutput(io.Discard) // Disable default logging to stdout
logger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile)
// Set up custom loggers with different severity levels
Error = log.New(io.MultiWriter(os.Stderr, os.Stdout),
fmt.Sprintf("%sERROR:%s ", "\033[0;101m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
Warning = log.New(os.Stdout,
fmt.Sprintf("%sWarning:%s ", "\033[0;93m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
Info = log.New(os.Stdout,
fmt.Sprintf("%sInfo:%s ", "\033[0;94m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
Success = log.New(os.Stdout,
fmt.Sprintf("%s✨ SUCCESS:%s ", "\033[0;92m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
// Initialize global stats
stats = GlobalStats{
Modifications: make([]processor.ModificationRecord, 0),
}
stats = GlobalStats{}
}
func main() {
// Define flags
fileModeFlag := flag.String("mode", "regex", "Processing mode: regex, xml, json")
xpathFlag := flag.String("xpath", "", "XPath expression (for XML mode)")
jsonpathFlag := flag.String("jsonpath", "", "JSONPath expression (for JSON mode)")
verboseFlag := flag.Bool("verbose", false, "Enable verbose output")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nOptions:\n")
@@ -103,174 +71,65 @@ func main() {
}
flag.Parse()
// Set up verbose mode
if !*verboseFlag {
// If not verbose, suppress Info level logs
Info.SetOutput(io.Discard)
}
args := flag.Args()
requiredArgCount := 3 // Default for regex mode
// XML/JSON modes need one fewer positional argument
if *fileModeFlag == "xml" || *fileModeFlag == "json" {
requiredArgCount = 2
}
if len(args) < requiredArgCount {
Error.Printf("%s mode requires %d arguments minimum", *fileModeFlag, requiredArgCount)
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "%s mode requires %d arguments minimum\n", *fileModeFlag, 3)
flag.Usage()
return
}
// Validate mode-specific parameters
if *fileModeFlag == "xml" && *xpathFlag == "" {
Error.Printf("XML mode requires an XPath expression with -xpath flag")
return
}
if *fileModeFlag == "json" && *jsonpathFlag == "" {
Error.Printf("JSON mode requires a JSONPath expression with -jsonpath flag")
return
}
// Get the appropriate pattern and expression based on mode
var regexPattern string
var luaExpr string
var pattern, luaExpr string
var filePatterns []string
// In regex mode, we need both pattern arguments
// In XML/JSON modes, we only need the lua expression from args
if *fileModeFlag == "regex" {
regexPattern = args[0]
pattern = args[0]
luaExpr = args[1]
filePatterns = args[2:]
// Process files with regex mode
processFilesWithRegex(regexPattern, luaExpr, filePatterns)
} else {
// XML/JSON modes
// For XML/JSON modes, pattern comes from flags
luaExpr = args[0]
filePatterns = args[1:]
// Prepare the Lua expression
originalLuaExpr := luaExpr
luaExpr = processor.BuildLuaScript(luaExpr)
if originalLuaExpr != luaExpr {
Info.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr)
}
// Expand file patterns with glob support
files, err := expandFilePatterns(filePatterns)
if err != nil {
Error.Printf("Error expanding file patterns: %v", err)
return
}
if len(files) == 0 {
Error.Printf("No files found matching the specified patterns")
return
}
// Create the processor based on mode
var proc processor.Processor
if *fileModeFlag == "xml" {
Info.Printf("Starting XML modifier with XPath '%s', expression '%s' on %d files",
*xpathFlag, luaExpr, len(files))
proc = processor.NewXMLProcessor(Info)
} else {
Info.Printf("Starting JSON modifier with JSONPath '%s', expression '%s' on %d files",
*jsonpathFlag, luaExpr, len(files))
proc = processor.NewJSONProcessor(Info)
}
var wg sync.WaitGroup
// Process each file
for _, file := range files {
wg.Add(1)
go func(file string) {
defer wg.Done()
Info.Printf("🔄 Processing file: %s", file)
// Pass the appropriate path expression as the pattern
var pattern string
if *fileModeFlag == "xml" {
pattern = *xpathFlag
} else {
pattern = *jsonpathFlag
}
modCount, matchCount, err := proc.Process(file, pattern, luaExpr, originalLuaExpr)
if err != nil {
Error.Printf("❌ Failed to process file %s: %v", file, err)
stats.FailedFiles++
} else {
Info.Printf("✅ Successfully processed file: %s", file)
stats.ProcessedFiles++
stats.TotalMatches += matchCount
stats.TotalModifications += modCount
}
}(file)
}
wg.Wait()
}
// Print summary of all modifications
printSummary(luaExpr)
}
// processFilesWithRegex handles regex mode pattern processing for multiple files
func processFilesWithRegex(regexPattern string, luaExpr string, filePatterns []string) {
// Prepare the Lua expression
originalLuaExpr := luaExpr
luaExpr = processor.BuildLuaScript(luaExpr)
if originalLuaExpr != luaExpr {
Info.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr)
}
// Handle special pattern modifications
originalPattern := regexPattern
patternModified := false
if strings.Contains(regexPattern, "!num") {
regexPattern = strings.ReplaceAll(regexPattern, "!num", "(-?\\d*\\.?\\d+)")
patternModified = true
}
// Make sure the regex can match across multiple lines by adding (?s) flag
if !strings.HasPrefix(regexPattern, "(?s)") {
regexPattern = "(?s)" + regexPattern
patternModified = true
}
if patternModified {
Info.Printf("Modified regex pattern from '%s' to '%s'", originalPattern, regexPattern)
}
// Compile the pattern for file processing
pattern, err := regexp.Compile(regexPattern)
if err != nil {
Error.Printf("Invalid regex pattern '%s': %v", regexPattern, err)
return
logger.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr)
}
// Expand file patterns with glob support
files, err := expandFilePatterns(filePatterns)
if err != nil {
Error.Printf("Error expanding file patterns: %v", err)
fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
return
}
if len(files) == 0 {
Error.Printf("No files found matching the specified patterns")
fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n")
return
}
Info.Printf("Starting regex modifier with pattern '%s', expression '%s' on %d files",
regexPattern, luaExpr, len(files))
// Create the regex processor
proc := processor.NewRegexProcessor(pattern, Info)
// Create the processor based on mode
var proc processor.Processor
switch *fileModeFlag {
case "regex":
proc = &processor.RegexProcessor{}
logger.Printf("Starting regex modifier with pattern '%s', expression '%s' on %d files",
pattern, luaExpr, len(files))
// case "xml":
// proc = &processor.XMLProcessor{}
// pattern = *xpathFlag
// logger.Printf("Starting XML modifier with XPath '%s', expression '%s' on %d files",
// pattern, luaExpr, len(files))
// case "json":
// proc = &processor.JSONProcessor{}
// pattern = *jsonpathFlag
// logger.Printf("Starting JSON modifier with JSONPath '%s', expression '%s' on %d files",
// pattern, luaExpr, len(files))
}
var wg sync.WaitGroup
// Process each file
@@ -278,13 +137,14 @@ func processFilesWithRegex(regexPattern string, luaExpr string, filePatterns []s
wg.Add(1)
go func(file string) {
defer wg.Done()
Info.Printf("🔄 Processing file: %s", file)
modCount, matchCount, err := proc.Process(file, regexPattern, luaExpr, originalLuaExpr)
logger.Printf("Processing file: %s", file)
modCount, matchCount, err := proc.Process(file, pattern, luaExpr)
if err != nil {
Error.Printf("Failed to process file %s: %v", file, err)
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
stats.FailedFiles++
} else {
Info.Printf("Successfully processed file: %s", file)
logger.Printf("Successfully processed file: %s", file)
stats.ProcessedFiles++
stats.TotalMatches += matchCount
stats.TotalModifications += modCount
@@ -292,58 +152,14 @@ func processFilesWithRegex(regexPattern string, luaExpr string, filePatterns []s
}(file)
}
wg.Wait()
}
// printSummary outputs a formatted summary of all modifications made
func printSummary(operation string) {
// Print summary
if stats.TotalModifications == 0 {
Warning.Printf("No modifications were made in any files")
return
fmt.Fprintf(os.Stderr, "No modifications were made in any files\n")
} else {
fmt.Printf("Operation complete! Modified %d values in %d/%d files\n",
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
}
Success.Printf("Operation complete! Modified %d values in %d/%d files using '%s'",
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles, operation)
// Group modifications by file for better readability
fileGroups := make(map[string][]processor.ModificationRecord)
for _, mod := range stats.Modifications {
fileGroups[mod.File] = append(fileGroups[mod.File], mod)
}
// Print modifications by file
for file, mods := range fileGroups {
Success.Printf("📄 %s: %d modifications", file, len(mods))
// Show some sample modifications (max 5 per file to avoid overwhelming output)
maxSamples := 5
if len(mods) > maxSamples {
for i := 0; i < maxSamples; i++ {
mod := mods[i]
Success.Printf(" %d. '%s' → '%s' %s",
i+1, limitString(mod.OldValue, 20), limitString(mod.NewValue, 20), mod.Context)
}
Success.Printf(" ... and %d more modifications", len(mods)-maxSamples)
} else {
for i, mod := range mods {
Success.Printf(" %d. '%s' → '%s' %s",
i+1, limitString(mod.OldValue, 20), limitString(mod.NewValue, 20), mod.Context)
}
}
}
// Print a nice visual indicator of success
if stats.TotalModifications > 0 {
Success.Printf("🎉 All done! Modified %d values successfully!", stats.TotalModifications)
}
}
// 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 expandFilePatterns(patterns []string) ([]string, error) {
@@ -360,7 +176,7 @@ func expandFilePatterns(patterns []string) ([]string, error) {
}
if len(files) > 0 {
Info.Printf("Found %d files to process", len(files))
logger.Printf("Found %d files to process", len(files))
}
return files, nil
}

View File

@@ -5,30 +5,18 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/PaesslerAG/jsonpath"
lua "github.com/yuin/gopher-lua"
)
// JSONProcessor implements the Processor interface using JSONPath
type JSONProcessor struct {
Logger Logger
}
// NewJSONProcessor creates a new JSONProcessor
func NewJSONProcessor(logger Logger) *JSONProcessor {
return &JSONProcessor{
Logger: logger,
}
}
// JSONProcessor implements the Processor interface for JSON documents
type JSONProcessor struct{}
// Process implements the Processor interface for JSONProcessor
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
// Use pattern as JSONPath expression
jsonPathExpr := pattern
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
// Read file content
fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath)
@@ -37,12 +25,9 @@ func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string,
}
fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the content
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, jsonPathExpr, luaExpr, originalExpr)
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
if err != nil {
return 0, 0, err
}
@@ -53,557 +38,271 @@ func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string,
if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d JSON value modifications to %s and saved (%d bytes)",
modCount, fullPath, len(modifiedContent))
}
} else if p.Logger != nil {
p.Logger.Printf("No modifications made to %s", fullPath)
}
return modCount, matchCount, nil
}
// ToLua implements the Processor interface for JSONProcessor
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
// For JSON, convert different types to appropriate Lua types
return nil
}
// FromLua implements the Processor interface for JSONProcessor
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Extract changes from Lua environment
return nil, nil
}
// ProcessContent implements the Processor interface for JSONProcessor
// It processes JSON content directly without file I/O
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) {
// Parse JSON
var jsonDoc interface{}
err := json.Unmarshal([]byte(content), &jsonDoc)
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
// Parse JSON document
var jsonData interface{}
err := json.Unmarshal([]byte(content), &jsonData)
if err != nil {
return "", 0, 0, fmt.Errorf("error parsing JSON: %v", err)
return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err)
}
// Log the JSONPath expression we're using
if p.Logger != nil {
p.Logger.Printf("JSON mode selected with JSONPath expression: %s", pattern)
}
// Initialize Lua state
L := lua.NewState()
defer L.Close()
// Setup Lua helper functions
if err := InitLuaHelpers(L); err != nil {
return "", 0, 0, err
}
// Setup JSON helpers
p.SetupJSONHelpers(L)
// Find matching nodes with simple JSONPath implementation
matchingPaths, err := p.findNodePaths(jsonDoc, pattern)
// Find nodes matching the JSONPath pattern
paths, values, err := p.findJSONPaths(jsonData, pattern)
if err != nil {
return "", 0, 0, fmt.Errorf("error finding JSON nodes: %v", err)
return content, 0, 0, fmt.Errorf("error executing JSONPath: %v", err)
}
if len(matchingPaths) == 0 {
if p.Logger != nil {
p.Logger.Printf("No JSON nodes matched JSONPath expression: %s", pattern)
}
matchCount := len(paths)
if matchCount == 0 {
return content, 0, 0, nil
}
if p.Logger != nil {
p.Logger.Printf("Found %d JSON nodes matching the path", len(matchingPaths))
// Initialize Lua
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 content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
}
// Process each node
matchCount := len(matchingPaths)
modificationCount := 0
modifications := []ModificationRecord{}
// Clone the document for modification
var modifiedDoc interface{}
modifiedBytes, err := json.Marshal(jsonDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
// Load helper functions
if err := InitLuaHelpers(L); err != nil {
return content, 0, 0, err
}
err = json.Unmarshal(modifiedBytes, &modifiedDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
}
// Apply modifications to each node
modCount := 0
for i, value := range values {
// Reset Lua state for each node
L.SetGlobal("v1", lua.LNil)
L.SetGlobal("s1", lua.LNil)
// For each matching path, extract value, apply Lua script, and update
for i, path := range matchingPaths {
// Extract the original value
originalValue, err := p.getValueAtPath(jsonDoc, path)
if err != nil || originalValue == nil {
if p.Logger != nil {
p.Logger.Printf("Error getting value at path %v: %v", path, err)
}
continue
}
if p.Logger != nil {
p.Logger.Printf("Processing node #%d at path %v with value: %v", i+1, path, originalValue)
}
// Process based on the value type
switch val := originalValue.(type) {
case float64:
// Set up Lua environment for numeric value
L.SetGlobal("v1", lua.LNumber(val))
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", val)))
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue
}
// Extract modified value
modVal := L.GetGlobal("v1")
if v, ok := modVal.(lua.LNumber); ok {
newValue := float64(v)
// Update the value in the document only if it changed
if newValue != val {
err := p.setValueAtPath(modifiedDoc, path, newValue)
if err != nil {
if p.Logger != nil {
p.Logger.Printf("Error updating value at path %v: %v", path, err)
}
continue
}
modificationCount++
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: fmt.Sprintf("%v", val),
NewValue: fmt.Sprintf("%v", newValue),
Operation: originalExpr,
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified numeric node #%d: %v -> %v", i+1, val, newValue)
}
}
}
case string:
// Set up Lua environment for string value
L.SetGlobal("s1", lua.LString(val))
// Try to convert to number if possible
if floatVal, err := strconv.ParseFloat(val, 64); err == nil {
L.SetGlobal("v1", lua.LNumber(floatVal))
} else {
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric
}
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue
}
// Check for modifications in string (s1) or numeric (v1) values
var newValue interface{}
modified := false
// Check if s1 was modified
sVal := L.GetGlobal("s1")
if s, ok := sVal.(lua.LString); ok && string(s) != val {
newValue = string(s)
modified = true
} else {
// Check if v1 was modified to a number
vVal := L.GetGlobal("v1")
if v, ok := vVal.(lua.LNumber); ok {
numStr := strconv.FormatFloat(float64(v), 'f', -1, 64)
if numStr != val {
newValue = numStr
modified = true
}
}
}
// Apply the modification if anything changed
if modified {
err := p.setValueAtPath(modifiedDoc, path, newValue)
if err != nil {
if p.Logger != nil {
p.Logger.Printf("Error updating value at path %v: %v", path, err)
}
continue
}
modificationCount++
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: val,
NewValue: fmt.Sprintf("%v", newValue),
Operation: originalExpr,
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified string node #%d: '%s' -> '%s'",
i+1, LimitString(val, 30), LimitString(fmt.Sprintf("%v", newValue), 30))
}
}
}
}
// Marshal the modified document back to JSON with indentation
if modificationCount > 0 {
modifiedJSON, err := json.MarshalIndent(modifiedDoc, "", " ")
// Convert to Lua variables
err = p.ToLua(L, value)
if err != nil {
return "", 0, 0, fmt.Errorf("error marshaling modified JSON: %v", err)
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d JSON node modifications", modificationCount)
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
}
return string(modifiedJSON), modificationCount, matchCount, nil
}
// Get modified value
result, err := p.FromLua(L)
if err != nil {
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
}
// If no modifications were made, return the original content
return content, 0, matchCount, nil
}
// findNodePaths implements a simplified JSONPath for finding paths to nodes
func (p *JSONProcessor) findNodePaths(doc interface{}, path string) ([][]interface{}, error) {
// Validate the path has proper syntax
if strings.Contains(path, "[[") || strings.Contains(path, "]]") {
return nil, fmt.Errorf("invalid JSONPath syntax: %s", path)
}
// Handle root element special case
if path == "$" {
return [][]interface{}{{doc}}, nil
}
// Split path into segments
segments := strings.Split(strings.TrimPrefix(path, "$."), ".")
// Start with the root
current := [][]interface{}{{doc}}
// Process each segment
for _, segment := range segments {
var next [][]interface{}
// Handle array notation [*]
if segment == "[*]" || strings.HasSuffix(segment, "[*]") {
baseName := strings.TrimSuffix(segment, "[*]")
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
switch v := item.(type) {
case map[string]interface{}:
if baseName == "" {
// [*] means all elements at this level
for _, val := range v {
if arr, ok := val.([]interface{}); ok {
for i, elem := range arr {
newPath := make([]interface{}, len(path)+2)
copy(newPath, path)
newPath[len(path)] = i // Array index
newPath[len(path)+1] = elem
next = append(next, newPath)
}
}
}
} else if arr, ok := v[baseName].([]interface{}); ok {
for i, elem := range arr {
newPath := make([]interface{}, len(path)+3)
copy(newPath, path)
newPath[len(path)] = baseName
newPath[len(path)+1] = i // Array index
newPath[len(path)+2] = elem
next = append(next, newPath)
}
}
case []interface{}:
for i, elem := range v {
newPath := make([]interface{}, len(path)+1)
copy(newPath, path)
newPath[len(path)-1] = i // Replace last elem with index
newPath[len(path)] = elem
next = append(next, newPath)
}
}
}
current = next
// Skip if value didn't change
if fmt.Sprintf("%v", value) == fmt.Sprintf("%v", result) {
continue
}
// Handle specific array indices
if strings.Contains(segment, "[") && strings.Contains(segment, "]") {
// Validate proper array syntax
if !regexp.MustCompile(`\[\d+\]$`).MatchString(segment) {
return nil, fmt.Errorf("invalid array index in JSONPath: %s", segment)
}
// Extract base name and index
baseName := segment[:strings.Index(segment, "[")]
idxStr := segment[strings.Index(segment, "[")+1 : strings.Index(segment, "]")]
idx, err := strconv.Atoi(idxStr)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", idxStr)
}
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
if obj, ok := item.(map[string]interface{}); ok {
if arr, ok := obj[baseName].([]interface{}); ok && idx < len(arr) {
newPath := make([]interface{}, len(path)+3)
copy(newPath, path)
newPath[len(path)] = baseName
newPath[len(path)+1] = idx
newPath[len(path)+2] = arr[idx]
next = append(next, newPath)
}
}
}
current = next
continue
// Apply the modification to the JSON data
err = p.updateJSONValue(jsonData, paths[i], result)
if err != nil {
return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err)
}
// Handle regular object properties
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
if obj, ok := item.(map[string]interface{}); ok {
if val, exists := obj[segment]; exists {
newPath := make([]interface{}, len(path)+2)
copy(newPath, path)
newPath[len(path)] = segment
newPath[len(path)+1] = val
next = append(next, newPath)
}
}
}
current = next
modCount++
}
return current, nil
// Convert the modified JSON back to a string
jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return content, modCount, matchCount, fmt.Errorf("error serializing JSON: %v", err)
}
return string(jsonBytes), modCount, matchCount, nil
}
// getValueAtPath extracts a value from a JSON document at the specified path
func (p *JSONProcessor) getValueAtPath(doc interface{}, path []interface{}) (interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("empty path")
// findJSONPaths finds all JSON paths and their values that match the given JSONPath expression
func (p *JSONProcessor) findJSONPaths(jsonData interface{}, pattern string) ([]string, []interface{}, error) {
// Extract all matching values using JSONPath
values, err := jsonpath.Get(pattern, jsonData)
if err != nil {
return nil, nil, err
}
// The last element in the path is the value itself
return path[len(path)-1], nil
}
// Convert values to a slice if it's not already
valuesSlice := []interface{}{}
paths := []string{}
// setValueAtPath updates a value in a JSON document at the specified path
func (p *JSONProcessor) setValueAtPath(doc interface{}, path []interface{}, newValue interface{}) error {
if len(path) < 2 {
return fmt.Errorf("path too short to update value")
}
// The path structure alternates: object/key/object/key/.../finalObject/finalKey/value
// We need to navigate to the object containing our key
// We'll get the parent object and the key to modify
// Find the parent object (second to last object) and the key (last object's property name)
// For the path structure, the parent is at index len-3 and key at len-2
if len(path) < 3 {
// Simple case: directly update the root object
rootObj, ok := doc.(map[string]interface{})
if !ok {
return fmt.Errorf("root is not an object, cannot update")
}
// Key should be a string
key, ok := path[len(path)-2].(string)
if !ok {
return fmt.Errorf("key is not a string: %v", path[len(path)-2])
}
rootObj[key] = newValue
return nil
}
// More complex case: we need to navigate to the parent object
parentIdx := len(path) - 3
keyIdx := len(path) - 2
// The actual key we need to modify
key, isString := path[keyIdx].(string)
keyInt, isInt := path[keyIdx].(int)
if !isString && !isInt {
return fmt.Errorf("key must be string or int, got %T", path[keyIdx])
}
// Get the parent object that contains the key
parent := path[parentIdx]
// If parent is a map, use string key
if parentMap, ok := parent.(map[string]interface{}); ok && isString {
parentMap[key] = newValue
return nil
}
// If parent is an array, use int key
if parentArray, ok := parent.([]interface{}); ok && isInt {
if keyInt < 0 || keyInt >= len(parentArray) {
return fmt.Errorf("array index %d out of bounds [0,%d)", keyInt, len(parentArray))
}
parentArray[keyInt] = newValue
return nil
}
return fmt.Errorf("cannot update value: parent is %T and key is %T", parent, path[keyIdx])
}
// SetupJSONHelpers adds JSON-specific helper functions to Lua
func (p *JSONProcessor) SetupJSONHelpers(L *lua.LState) {
// Helper to get type of JSON value
L.SetGlobal("json_type", L.NewFunction(func(L *lua.LState) int {
// Get the value passed to the function
val := L.Get(1)
// Determine type
switch val.Type() {
case lua.LTNil:
L.Push(lua.LString("null"))
case lua.LTBool:
L.Push(lua.LString("boolean"))
case lua.LTNumber:
L.Push(lua.LString("number"))
case lua.LTString:
L.Push(lua.LString("string"))
case lua.LTTable:
// Could be object or array - check for numeric keys
isArray := true
table := val.(*lua.LTable)
table.ForEach(func(key, value lua.LValue) {
if key.Type() != lua.LTNumber {
isArray = false
}
})
if isArray {
L.Push(lua.LString("array"))
} else {
L.Push(lua.LString("object"))
}
default:
L.Push(lua.LString("unknown"))
}
return 1
}))
}
// jsonToLua converts a Go JSON value to a Lua value
func (p *JSONProcessor) jsonToLua(L *lua.LState, val interface{}) lua.LValue {
if val == nil {
return lua.LNil
}
switch v := val.(type) {
case bool:
return lua.LBool(v)
case float64:
return lua.LNumber(v)
case string:
return lua.LString(v)
switch v := values.(type) {
case []interface{}:
arr := L.NewTable()
for i, item := range v {
arr.RawSetInt(i+1, p.jsonToLua(L, item))
valuesSlice = v
// Generate paths for array elements
// This is simplified - for complex JSONPath expressions you might
// need a more robust approach to generate the exact path
basePath := pattern
if strings.Contains(pattern, "[*]") || strings.HasSuffix(pattern, ".*") {
basePath = strings.Replace(pattern, "[*]", "", -1)
basePath = strings.Replace(basePath, ".*", "", -1)
for i := 0; i < len(v); i++ {
paths = append(paths, fmt.Sprintf("%s[%d]", basePath, i))
}
} else {
for range v {
paths = append(paths, pattern)
}
}
return arr
case map[string]interface{}:
obj := L.NewTable()
for k, item := range v {
obj.RawSetString(k, p.jsonToLua(L, item))
}
return obj
default:
// For unknown types, convert to string representation
return lua.LString(fmt.Sprintf("%v", val))
valuesSlice = append(valuesSlice, v)
paths = append(paths, pattern)
}
return paths, valuesSlice, nil
}
// luaToJSON converts a Lua value to a Go JSON-compatible value
func (p *JSONProcessor) luaToJSON(val lua.LValue) interface{} {
switch val.Type() {
case lua.LTNil:
return nil
case lua.LTBool:
return lua.LVAsBool(val)
case lua.LTNumber:
return float64(val.(lua.LNumber))
case lua.LTString:
return val.String()
case lua.LTTable:
table := val.(*lua.LTable)
// updateJSONValue updates a value in the JSON data structure at the given path
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
// This is a simplified approach - for a production system you'd need a more robust solution
// that can handle all JSONPath expressions
parts := strings.Split(path, ".")
current := jsonData
// Check if it's an array or an object
isArray := true
maxN := 0
// Traverse the JSON structure
for i, part := range parts {
if i == len(parts)-1 {
// Last part, set the value
if strings.HasSuffix(part, "]") {
// Handle array access
arrayPart := part[:strings.Index(part, "[")]
indexPart := part[strings.Index(part, "[")+1 : strings.Index(part, "]")]
index, err := strconv.Atoi(indexPart)
if err != nil {
return fmt.Errorf("invalid array index: %s", indexPart)
}
table.ForEach(func(key, _ lua.LValue) {
if key.Type() == lua.LTNumber {
n := int(key.(lua.LNumber))
if n > maxN {
maxN = n
// Get the array
var array []interface{}
if arrayPart == "" {
// Direct array access
array, _ = current.([]interface{})
} else {
// Access array property
obj, _ := current.(map[string]interface{})
array, _ = obj[arrayPart].([]interface{})
}
// Set the value
if index >= 0 && index < len(array) {
array[index] = newValue
}
} else {
isArray = false
// Handle object property
obj, _ := current.(map[string]interface{})
obj[part] = newValue
}
})
if isArray && maxN > 0 {
// It's an array
arr := make([]interface{}, maxN)
for i := 1; i <= maxN; i++ {
item := table.RawGetInt(i)
if item != lua.LNil {
arr[i-1] = p.luaToJSON(item)
}
}
return arr
} else {
// It's an object
obj := make(map[string]interface{})
table.ForEach(func(key, value lua.LValue) {
if key.Type() == lua.LTString {
obj[key.String()] = p.luaToJSON(value)
} else {
// Convert key to string if it's not already
obj[fmt.Sprintf("%v", key)] = p.luaToJSON(value)
}
})
return obj
break
}
// Not the last part, continue traversing
if strings.HasSuffix(part, "]") {
// Handle array access
arrayPart := part[:strings.Index(part, "[")]
indexPart := part[strings.Index(part, "[")+1 : strings.Index(part, "]")]
index, err := strconv.Atoi(indexPart)
if err != nil {
return fmt.Errorf("invalid array index: %s", indexPart)
}
// Get the array
var array []interface{}
if arrayPart == "" {
// Direct array access
array, _ = current.([]interface{})
} else {
// Access array property
obj, _ := current.(map[string]interface{})
array, _ = obj[arrayPart].([]interface{})
}
// Continue with the array element
if index >= 0 && index < len(array) {
current = array[index]
}
} else {
// Handle object property
obj, _ := current.(map[string]interface{})
current = obj[part]
}
default:
// For functions, userdata, etc., convert to string
return val.String()
}
return nil
}
// ToLua converts JSON values to Lua variables
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
switch v := data.(type) {
case float64:
L.SetGlobal("v1", lua.LNumber(v))
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
case int:
L.SetGlobal("v1", lua.LNumber(v))
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%d", v)))
case string:
L.SetGlobal("s1", lua.LString(v))
// Try to convert to number if possible
if val, err := strconv.ParseFloat(v, 64); err == nil {
L.SetGlobal("v1", lua.LNumber(val))
} else {
L.SetGlobal("v1", lua.LNumber(0))
}
case bool:
if v {
L.SetGlobal("v1", lua.LNumber(1))
} else {
L.SetGlobal("v1", lua.LNumber(0))
}
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
default:
// For complex types, convert to string
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
L.SetGlobal("v1", lua.LNumber(0))
}
return nil
}
// FromLua retrieves values from Lua
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Check if string variable was modified
s1 := L.GetGlobal("s1")
if s1 != lua.LNil {
if s1Str, ok := s1.(lua.LString); ok {
// Try to convert to number if it's numeric
if val, err := strconv.ParseFloat(string(s1Str), 64); err == nil {
return val, nil
}
// If it's "true" or "false", convert to boolean
if string(s1Str) == "true" {
return true, nil
}
if string(s1Str) == "false" {
return false, nil
}
return string(s1Str), nil
}
}
// Check if numeric variable was modified
v1 := L.GetGlobal("v1")
if v1 != lua.LNil {
if v1Num, ok := v1.(lua.LNumber); ok {
return float64(v1Num), nil
}
}
// Default return nil
return nil, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,11 @@ import (
// Processor defines the interface for all file processors
type Processor interface {
// Process handles processing a file with the given pattern and Lua expression
Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error)
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, originalExpr string) (string, int, int, 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
@@ -76,6 +76,29 @@ func LimitString(s string, maxLen int) string {
return s[:maxLen-3] + "..."
}
// BuildLuaScript prepares a Lua expression from shorthand notation
func BuildLuaScript(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
}
// Max returns the maximum of two integers
func Max(a, b int) int {
if a > b {

View File

@@ -12,26 +12,10 @@ import (
)
// RegexProcessor implements the Processor interface using regex patterns
type RegexProcessor struct {
CompiledPattern *regexp.Regexp
Logger Logger
}
// Logger interface abstracts logging functionality
type Logger interface {
Printf(format string, v ...interface{})
}
// NewRegexProcessor creates a new RegexProcessor with the given pattern
func NewRegexProcessor(pattern *regexp.Regexp, logger Logger) *RegexProcessor {
return &RegexProcessor{
CompiledPattern: pattern,
Logger: logger,
}
}
type RegexProcessor struct{}
// Process implements the Processor interface for RegexProcessor
func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
// Read file content
fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath)
@@ -40,32 +24,19 @@ func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string
}
fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the content with regex
result, modCount, matchCount, err := p.ProcessContent(fileContent, luaExpr, filename, originalExpr)
// Process the content
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
if err != nil {
return 0, 0, err
}
if modCount == 0 {
if p.Logger != nil {
p.Logger.Printf("No modifications made to %s - pattern didn't match any content", fullPath)
// 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 0, 0, nil
}
// Write the modified content back
err = os.WriteFile(fullPath, []byte(result), 0644)
if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d modifications to %s and saved (%d bytes)",
modCount, fullPath, len(result))
}
return modCount, matchCount, nil
@@ -109,14 +80,22 @@ func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
vLuaVal := L.GetGlobal(vVarName)
sLuaVal := L.GetGlobal(sVarName)
// Get the v variable if it exists
// First check string variables (s1, s2, etc.) as they should have priority
if sLuaVal != lua.LNil {
if sStr, ok := sLuaVal.(lua.LString); ok {
newStrVal := string(sStr)
modifications[i] = newStrVal
continue
}
}
// Then check numeric variables (v1, v2, etc.)
if vLuaVal != lua.LNil {
switch v := vLuaVal.(type) {
case lua.LNumber:
// Convert numeric value to string
newNumVal := strconv.FormatFloat(float64(v), 'f', -1, 64)
modifications[i] = newNumVal
// We found a value, continue to next capture group
continue
case lua.LString:
// Use string value directly
@@ -130,113 +109,70 @@ func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
continue
}
}
// Try the s variable if v variable wasn't found or couldn't be used
if sLuaVal != lua.LNil {
if sStr, ok := sLuaVal.(lua.LString); ok {
newStrVal := string(sStr)
modifications[i] = newStrVal
continue
}
}
}
if p.Logger != nil {
p.Logger.Printf("Final modifications map: %v", modifications)
}
return modifications, nil
}
// ProcessContent applies regex replacement with Lua processing
func (p *RegexProcessor) ProcessContent(data string, luaExpr string, filename string, originalExpr string) (string, int, int, error) {
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
// Handle special pattern modifications
if !strings.HasPrefix(pattern, "(?s)") {
pattern = "(?s)" + pattern
}
compiledPattern, err := regexp.Compile(pattern)
if err != nil {
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
}
L := lua.NewState()
defer L.Close()
// Initialize Lua environment
modificationCount := 0
matchCount := 0
modifications := []ModificationRecord{}
// Load math library
L.Push(L.GetGlobal("require"))
L.Push(lua.LString("math"))
if err := L.PCall(1, 1, nil); err != nil {
if p.Logger != nil {
p.Logger.Printf("Failed to load Lua math library: %v", err)
}
return data, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
return content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
}
// Initialize helper functions
if err := InitLuaHelpers(L); err != nil {
return data, 0, 0, err
return content, 0, 0, err
}
// Process all regex matches
result := p.CompiledPattern.ReplaceAllStringFunc(data, func(match string) string {
result := compiledPattern.ReplaceAllStringFunc(content, func(match string) string {
matchCount++
captures := p.CompiledPattern.FindStringSubmatch(match)
captures := compiledPattern.FindStringSubmatch(match)
if len(captures) <= 1 {
// No capture groups, return unchanged
if p.Logger != nil {
p.Logger.Printf("Match found but no capture groups: %s", LimitString(match, 50))
}
return match
}
if p.Logger != nil {
p.Logger.Printf("Match found: %s", LimitString(match, 50))
}
// Pass the captures to Lua environment
if err := p.ToLua(L, captures); err != nil {
if p.Logger != nil {
p.Logger.Printf("Failed to set Lua variables: %v", err)
}
return match
}
// Debug: print the Lua variables before execution
if p.Logger != nil {
v1 := L.GetGlobal("v1")
s1 := L.GetGlobal("s1")
p.Logger.Printf("Before Lua: v1=%v, s1=%v", v1, s1)
}
// Execute the user's Lua code
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for match '%s': %v", LimitString(match, 50), err)
}
return match // Return unchanged on error
}
// Debug: print the Lua variables after execution
if p.Logger != nil {
v1 := L.GetGlobal("v1")
s1 := L.GetGlobal("s1")
p.Logger.Printf("After Lua: v1=%v, s1=%v", v1, s1)
}
// Get modifications from Lua
modResult, err := p.FromLua(L)
if err != nil {
if p.Logger != nil {
p.Logger.Printf("Failed to get modifications from Lua: %v", err)
}
return match
}
// Debug: print the modifications detected
if p.Logger != nil {
p.Logger.Printf("Modifications detected: %v", modResult)
}
// Apply modifications to the matched text
modsMap, ok := modResult.(map[int]string)
if !ok || len(modsMap) == 0 {
p.Logger.Printf("No modifications detected after Lua script execution")
return match // No changes
}
@@ -244,57 +180,7 @@ func (p *RegexProcessor) ProcessContent(data string, luaExpr string, filename st
result := match
for i, newVal := range modsMap {
oldVal := captures[i+1]
// Special handling for empty capture groups
if oldVal == "" {
// Find the position where the empty capture group should be
// by analyzing the regex pattern and current match
parts := p.CompiledPattern.SubexpNames()
if i+1 < len(parts) && parts[i+1] != "" {
// Named capture groups
subPattern := fmt.Sprintf("(?P<%s>)", parts[i+1])
emptyGroupPattern := regexp.MustCompile(subPattern)
if loc := emptyGroupPattern.FindStringIndex(result); loc != nil {
// Insert the new value at the capture group location
result = result[:loc[0]] + newVal + result[loc[1]:]
}
} else {
// For unnamed capture groups, we need to find where they would be in the regex
// This is a simplification that might not work for complex regex patterns
// but should handle the test case with <value></value>
tagPattern := regexp.MustCompile("<value></value>")
if loc := tagPattern.FindStringIndex(result); loc != nil {
// Replace the empty tag content with our new value
result = result[:loc[0]+7] + newVal + result[loc[1]-8:]
}
}
} else {
// Normal replacement for non-empty capture groups
p.Logger.Printf("Replacing '%s' with '%s' in '%s'", oldVal, newVal, result)
result = strings.Replace(result, oldVal, newVal, 1)
p.Logger.Printf("After replacement: '%s'", result)
}
// Extract a bit of context from the match for better reporting
contextStart := Max(0, strings.Index(match, oldVal)-10)
contextLength := Min(30, len(match)-contextStart)
if contextStart+contextLength > len(match) {
contextLength = len(match) - contextStart
}
contextStr := "..." + match[contextStart:contextStart+contextLength] + "..."
// Log the modification
if p.Logger != nil {
p.Logger.Printf("Modified value [%d]: '%s' → '%s'", i+1, LimitString(oldVal, 30), LimitString(newVal, 30))
}
// Record the modification for summary
modifications = append(modifications, ModificationRecord{
File: filename,
OldValue: oldVal,
NewValue: newVal,
Operation: originalExpr,
Context: fmt.Sprintf("(in %s)", LimitString(contextStr, 30)),
})
result = strings.Replace(result, oldVal, newVal, 1)
}
modificationCount++
@@ -303,26 +189,3 @@ func (p *RegexProcessor) ProcessContent(data string, luaExpr string, filename st
return result, modificationCount, matchCount, nil
}
// BuildLuaScript creates a complete Lua script from the expression
func BuildLuaScript(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
}

View File

@@ -24,315 +24,283 @@ func normalizeWhitespace(s string) string {
return re.ReplaceAllString(strings.TrimSpace(s), " ")
}
func TestBuildLuaScript(t *testing.T) {
cases := []struct {
input string
expected string
}{
{"s1 .. '_suffix'", "v1 = s1 .. '_suffix'"},
{"v1 * 1.5", "v1 = v1 * 1.5"},
{"v1 + 10", "v1 = v1 + 10"},
{"v1 * 2", "v1 = v1 * 2"},
{"v1 * v2", "v1 = v1 * v2"},
{"v1 / v2", "v1 = v1 / v2"},
}
for _, c := range cases {
result := BuildLuaScript(c.input)
if result != c.expected {
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
}
}
}
func TestSimpleValueMultiplication(t *testing.T) {
content := `
<config>
<item>
<value>100</value>
</item>
</config>
`
expected := `
<config>
<item>
<value>150</value>
</item>
</config>
`
content := `<config>
<item>
<value>100</value>
</item>
</config>`
// Create a regex pattern with the (?s) flag for multiline matching
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{T: t})
luaExpr := BuildLuaScript("*1.5")
expected := `<config>
<item>
<value>150</value>
</item>
</config>`
// Enable verbose logging for this test
t.Logf("Running test with regex pattern: %s", regex.String())
t.Logf("Original content: %s", content)
t.Logf("Lua expression: %s", luaExpr)
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+)</value>`, "v1 = v1*1.5")
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "*1.5")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 1 {
t.Errorf("Expected 1 match, got %d", matches)
}
t.Logf("Modified content: %s", modifiedContent)
t.Logf("Expected content: %s", expected)
if mods != 1 {
t.Errorf("Expected 1 modification, got %d", mods)
}
// Compare normalized content
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestShorthandNotation(t *testing.T) {
content := `
<config>
<item>
<value>100</value>
</item>
</config>
`
expected := `
<config>
<item>
<value>150</value>
</item>
</config>
`
content := `<config>
<item>
<value>100</value>
</item>
</config>`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("v1 * 1.5") // Use direct assignment syntax
expected := `<config>
<item>
<value>150</value>
</item>
</config>`
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+)</value>`, "v1*1.5")
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 * 1.5")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 1 {
t.Errorf("Expected 1 match, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 1 {
t.Errorf("Expected 1 modification, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestShorthandNotationFloats(t *testing.T) {
content := `
<config>
<item>
<value>132.671327</value>
</item>
</config>
`
expected := `
<config>
<item>
<value>176.01681007940928</value>
</item>
</config>
`
content := `<config>
<item>
<value>10.5</value>
</item>
</config>`
regex := regexp.MustCompile(`(?s)<value>(\d*\.?\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("v1 * 1.32671327") // Use direct assignment syntax
expected := `<config>
<item>
<value>15.75</value>
</item>
</config>`
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+\.\d+)</value>`, "v1*1.5")
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 * 1.32671327")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 1 {
t.Errorf("Expected 1 match, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 1 {
t.Errorf("Expected 1 modification, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestArrayNotation(t *testing.T) {
content := `
<config>
<item>
<value>100</value>
</item>
</config>
`
expected := `
<config>
<item>
<value>150</value>
</item>
</config>
`
content := `<config>
<prices>
<price>10</price>
<price>20</price>
<price>30</price>
</prices>
</config>`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("v1 = v1 * 1.5") // Use direct assignment syntax
expected := `<config>
<prices>
<price>20</price>
<price>40</price>
<price>60</price>
</prices>
</config>`
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `(?s)<price>(\d+)</price>`, "v1*2")
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 = v1 * 1.5")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 3 {
t.Errorf("Expected 3 matches, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 3 {
t.Errorf("Expected 3 modifications, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestMultipleMatches(t *testing.T) {
content := `
<config>
<item>
<value>100</value>
</item>
<item>
<value>200</value>
</item>
<item> <value>300</value> </item>
</config>
`
expected := `
<config>
<item>
<value>150</value>
</item>
<item>
<value>300</value>
</item>
<item> <value>450</value> </item>
</config>
`
content := `<data>
<entry>50</entry>
<entry>100</entry>
<entry>200</entry>
</data>`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("*1.5")
expected := `<data>
<entry>100</entry>
<entry>200</entry>
<entry>400</entry>
</data>`
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `<entry>(\d+)</entry>`, "v1*2")
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "*1.5")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 3 {
t.Errorf("Expected 3 matches, got %d", matchCount)
}
if modCount != 3 {
t.Errorf("Expected 3 modifications, got %d", modCount)
if matches != 3 {
t.Errorf("Expected 3 matches, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 3 {
t.Errorf("Expected 3 modifications, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
// Test string operations
content = `<data>
<name>John</name>
<name>Mary</name>
</data>`
expected = `<data>
<name>John_modified</name>
<name>Mary_modified</name>
</data>`
result, mods, matches, err = p.ProcessContent(content, `<name>([A-Za-z]+)</name>`, `s1 = s1 .. "_modified"`)
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
if matches != 2 {
t.Errorf("Expected 2 matches, got %d", matches)
}
if mods != 2 {
t.Errorf("Expected 2 modifications, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestMultipleCaptureGroups(t *testing.T) {
content := `
<config>
<item>
<value>10</value>
<multiplier>5</multiplier>
</item>
</config>
`
expected := `
<config>
<item>
<value>50</value>
<multiplier>5</multiplier>
</item>
</config>
`
func TestStringOperations(t *testing.T) {
content := `<users>
<user>John</user>
<user>Mary</user>
</users>`
// Use (?s) flag to match across multiple lines
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>.*?<multiplier>(\d+)</multiplier>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("v1 = v1 * v2") // Use direct assignment syntax
expected := `<users>
<user>JOHN</user>
<user>MARY</user>
</users>`
// Verify the regex matches before processing
matches := regex.FindStringSubmatch(content)
if len(matches) <= 1 {
t.Fatalf("Regex didn't match any capture groups in test input: %v", content)
}
p := &RegexProcessor{}
// Convert names to uppercase using Lua string function
result, mods, matches, err := p.ProcessContent(content, `<user>([A-Za-z]+)</user>`, `s1 = string.upper(s1)`)
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 = v1 * v2")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 2 {
t.Errorf("Expected 2 matches, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 2 {
t.Errorf("Expected 2 modifications, got %d", mods)
}
}
func TestModifyingMultipleValues(t *testing.T) {
content := `
<config>
<item>
<value>50</value>
<multiplier>3</multiplier>
<divider>2</divider>
</item>
</config>
`
expected := `
<config>
<item>
<value>75</value>
<multiplier>5</multiplier>
<divider>1</divider>
</item>
</config>
`
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>.*?<multiplier>(\d+)</multiplier>.*?<divider>(\d+)</divider>`)
processor := NewRegexProcessor(regex, &TestLogger{})
luaExpr := BuildLuaScript("v1 = v1 * v2 / v3; v2 = min(v2 * 2, 5); v3 = max(1, v3 / 2)")
// Test string concatenation
content = `<products>
<product>Apple</product>
<product>Banana</product>
</products>`
expected = `<products>
<product>Apple_fruit</product>
<product>Banana_fruit</product>
</products>`
result, mods, matches, err = p.ProcessContent(content, `<product>([A-Za-z]+)</product>`, `s1 = s1 .. "_fruit"`)
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test",
"v1 = v1 * v2 / v3; v2 = min(v2 * 2, 5); v3 = max(1, v3 / 2)")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
// Verify match and modification counts
if matchCount != 1 {
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
if matches != 2 {
t.Errorf("Expected 2 matches, got %d", matches)
}
normalizedModified := normalizeWhitespace(modifiedContent)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
if mods != 2 {
t.Errorf("Expected 2 modifications, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
@@ -356,10 +324,10 @@ func TestDecimalValues(t *testing.T) {
`
regex := regexp.MustCompile(`(?s)<value>([0-9.]+)</value>.*?<multiplier>([0-9.]+)</multiplier>`)
processor := NewRegexProcessor(regex, &TestLogger{})
p := &RegexProcessor{}
luaExpr := BuildLuaScript("v1 = v1 * v2")
modifiedContent, _, _, err := processor.ProcessContent(content, luaExpr, "test", "v1 = v1 * v2")
modifiedContent, _, _, err := p.ProcessContent(content, regex.String(), luaExpr)
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
@@ -389,10 +357,10 @@ func TestLuaMathFunctions(t *testing.T) {
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
p := &RegexProcessor{}
luaExpr := BuildLuaScript("v1 = math.sqrt(v1)")
modifiedContent, _, _, err := processor.ProcessContent(content, luaExpr, "test", "v1 = math.sqrt(v1)")
modifiedContent, _, _, err := p.ProcessContent(content, regex.String(), luaExpr)
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
@@ -422,10 +390,10 @@ func TestDirectAssignment(t *testing.T) {
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{})
p := &RegexProcessor{}
luaExpr := BuildLuaScript("=0")
modifiedContent, _, _, err := processor.ProcessContent(content, luaExpr, "test", "=0")
modifiedContent, _, _, err := p.ProcessContent(content, regex.String(), luaExpr)
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
@@ -457,10 +425,10 @@ func TestStringAndNumericOperations(t *testing.T) {
},
{
name: "Basic string manipulation",
input: "<n>test</n>",
regexPattern: "<n>(.*?)</n>",
input: "<name>test</name>",
regexPattern: "<name>(.*?)</name>",
luaExpression: "s1 = string.upper(s1)",
expectedOutput: "<n>TEST</n>",
expectedOutput: "<name>TEST</name>",
expectedMods: 1,
},
{
@@ -484,12 +452,12 @@ func TestStringAndNumericOperations(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Compile the regex pattern with multiline support
pattern := regexp.MustCompile("(?s)" + tt.regexPattern)
processor := NewRegexProcessor(pattern, &TestLogger{})
pattern := "(?s)" + tt.regexPattern
p := &RegexProcessor{}
luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function
result, modCount, _, err := processor.ProcessContent(tt.input, luaExpr, "test", tt.luaExpression)
result, modCount, _, err := p.ProcessContent(tt.input, pattern, luaExpr)
if err != nil {
t.Fatalf("Process function failed: %v", err)
}
@@ -553,12 +521,12 @@ func TestEdgeCases(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Make sure the regex can match across multiple lines
pattern := regexp.MustCompile("(?s)" + tt.regexPattern)
processor := NewRegexProcessor(pattern, &TestLogger{})
pattern := "(?s)" + tt.regexPattern
p := &RegexProcessor{}
luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function
result, modCount, _, err := processor.ProcessContent(tt.input, luaExpr, "test", tt.luaExpression)
result, modCount, _, err := p.ProcessContent(tt.input, pattern, luaExpr)
if err != nil {
t.Fatalf("Process function failed: %v", err)
}
@@ -574,32 +542,3 @@ func TestEdgeCases(t *testing.T) {
})
}
}
func TestBuildLuaScript(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"*1.5", "v1 = v1*1.5"},
{"/2", "v1 = v1/2"},
{"+10", "v1 = v1+10"},
{"-5", "v1 = v1-5"},
{"^2", "v1 = v1^2"},
{"%2", "v1 = v1%2"},
{"=100", "v1 =100"},
{"v1 * 2", "v1 = v1 * 2"},
{"v1 + v2", "v1 = v1 + v2"},
{"math.max(v1, 100)", "v1 = math.max(v1, 100)"},
// Added from main_test.go
{"s1 .. '_suffix'", "v1 = s1 .. '_suffix'"},
{"v1 * v2", "v1 = v1 * v2"},
{"s1 .. s2", "v1 = s1 .. s2"},
}
for _, tc := range testCases {
result := BuildLuaScript(tc.input)
if result != tc.expected {
t.Errorf("BuildLuaScript(%q): expected %q, got %q", tc.input, tc.expected, result)
}
}
}

View File

@@ -4,30 +4,17 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/antchfx/xmlquery"
lua "github.com/yuin/gopher-lua"
)
// XMLProcessor implements the Processor interface using XPath
type XMLProcessor struct {
Logger Logger
}
// NewXMLProcessor creates a new XMLProcessor
func NewXMLProcessor(logger Logger) *XMLProcessor {
return &XMLProcessor{
Logger: logger,
}
}
// XMLProcessor implements the Processor interface for XML documents
type XMLProcessor struct{}
// Process implements the Processor interface for XMLProcessor
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
// Use pattern as XPath expression
xpathExpr := pattern
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
// Read file content
fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath)
@@ -36,12 +23,9 @@ func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string,
}
fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the content
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, xpathExpr, luaExpr, originalExpr)
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
if err != nil {
return 0, 0, err
}
@@ -52,403 +36,182 @@ func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string,
if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d XML node modifications to %s and saved (%d bytes)",
modCount, fullPath, len(modifiedContent))
}
}
return modCount, matchCount, nil
}
// ToLua implements the Processor interface for XMLProcessor
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
// Currently not used directly as this is handled in Process
return nil
}
// FromLua implements the Processor interface for XMLProcessor
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Currently not used directly as this is handled in Process
return nil, nil
}
// XMLNodeToString converts an XML node to a string representation
func (p *XMLProcessor) XMLNodeToString(node *xmlquery.Node) string {
// Use a simple string representation for now
var sb strings.Builder
// Start tag with attributes
if node.Type == xmlquery.ElementNode {
sb.WriteString("<")
sb.WriteString(node.Data)
// Add attributes
for _, attr := range node.Attr {
sb.WriteString(" ")
sb.WriteString(attr.Name.Local)
sb.WriteString("=\"")
sb.WriteString(attr.Value)
sb.WriteString("\"")
}
// If self-closing
if node.FirstChild == nil {
sb.WriteString("/>")
return sb.String()
}
sb.WriteString(">")
} else if node.Type == xmlquery.TextNode {
// Just write the text content
sb.WriteString(node.Data)
return sb.String()
} else if node.Type == xmlquery.CommentNode {
// Write comment
sb.WriteString("<!--")
sb.WriteString(node.Data)
sb.WriteString("-->")
return sb.String()
}
// Add children
for child := node.FirstChild; child != nil; child = child.NextSibling {
sb.WriteString(p.XMLNodeToString(child))
}
// End tag for elements
if node.Type == xmlquery.ElementNode {
sb.WriteString("</")
sb.WriteString(node.Data)
sb.WriteString(">")
}
return sb.String()
}
// NodeToLuaTable creates a Lua table from an XML node
func (p *XMLProcessor) NodeToLuaTable(L *lua.LState, node *xmlquery.Node) lua.LValue {
nodeTable := L.NewTable()
// Add node name
L.SetField(nodeTable, "name", lua.LString(node.Data))
// Add node type
switch node.Type {
case xmlquery.ElementNode:
L.SetField(nodeTable, "type", lua.LString("element"))
case xmlquery.TextNode:
L.SetField(nodeTable, "type", lua.LString("text"))
case xmlquery.AttributeNode:
L.SetField(nodeTable, "type", lua.LString("attribute"))
case xmlquery.CommentNode:
L.SetField(nodeTable, "type", lua.LString("comment"))
default:
L.SetField(nodeTable, "type", lua.LString("other"))
}
// Add node text content if it's a text node
if node.Type == xmlquery.TextNode {
L.SetField(nodeTable, "content", lua.LString(node.Data))
}
// Add attributes if it's an element node
if node.Type == xmlquery.ElementNode && len(node.Attr) > 0 {
attrsTable := L.NewTable()
for _, attr := range node.Attr {
L.SetField(attrsTable, attr.Name.Local, lua.LString(attr.Value))
}
L.SetField(nodeTable, "attributes", attrsTable)
}
// Add children if any
if node.FirstChild != nil {
childrenTable := L.NewTable()
i := 1
for child := node.FirstChild; child != nil; child = child.NextSibling {
// Skip empty text nodes (whitespace)
if child.Type == xmlquery.TextNode && strings.TrimSpace(child.Data) == "" {
continue
}
childTable := p.NodeToLuaTable(L, child)
childrenTable.RawSetInt(i, childTable)
i++
}
L.SetField(nodeTable, "children", childrenTable)
}
return nodeTable
}
// GetModifiedNode retrieves a modified node from Lua
func (p *XMLProcessor) GetModifiedNode(L *lua.LState, originalNode *xmlquery.Node) (*xmlquery.Node, bool) {
// Check if we have a node global with changes
nodeTable := L.GetGlobal("node")
if nodeTable == lua.LNil || nodeTable.Type() != lua.LTTable {
return originalNode, false
}
// Clone the node since we don't want to modify the original
clonedNode := *originalNode
// For text nodes, check if content was changed
if originalNode.Type == xmlquery.TextNode {
contentField := L.GetField(nodeTable.(*lua.LTable), "content")
if contentField != lua.LNil {
if strContent, ok := contentField.(lua.LString); ok {
if string(strContent) != originalNode.Data {
clonedNode.Data = string(strContent)
return &clonedNode, true
}
}
}
return originalNode, false
}
// For element nodes, attributes might have been changed
if originalNode.Type == xmlquery.ElementNode {
attrsField := L.GetField(nodeTable.(*lua.LTable), "attributes")
if attrsField != lua.LNil && attrsField.Type() == lua.LTTable {
attrsTable := attrsField.(*lua.LTable)
// Check if any attributes changed
changed := false
for _, attr := range originalNode.Attr {
newValue := L.GetField(attrsTable, attr.Name.Local)
if newValue != lua.LNil {
if strValue, ok := newValue.(lua.LString); ok {
if string(strValue) != attr.Value {
// Create a new attribute with the changed value
for i, a := range clonedNode.Attr {
if a.Name.Local == attr.Name.Local {
clonedNode.Attr[i].Value = string(strValue)
changed = true
}
}
}
}
}
}
if changed {
return &clonedNode, true
}
}
}
// No changes detected
return originalNode, false
}
// SetupXMLHelpers adds XML-specific helper functions to Lua
func (p *XMLProcessor) SetupXMLHelpers(L *lua.LState) {
// Helper function to create a new XML node
L.SetGlobal("new_node", L.NewFunction(func(L *lua.LState) int {
nodeName := L.CheckString(1)
nodeTable := L.NewTable()
L.SetField(nodeTable, "name", lua.LString(nodeName))
L.SetField(nodeTable, "type", lua.LString("element"))
L.SetField(nodeTable, "attributes", L.NewTable())
L.SetField(nodeTable, "children", L.NewTable())
L.Push(nodeTable)
return 1
}))
// Helper function to set an attribute
L.SetGlobal("set_attr", L.NewFunction(func(L *lua.LState) int {
nodeTable := L.CheckTable(1)
attrName := L.CheckString(2)
attrValue := L.CheckString(3)
attrsTable := L.GetField(nodeTable, "attributes")
if attrsTable == lua.LNil {
attrsTable = L.NewTable()
L.SetField(nodeTable, "attributes", attrsTable)
}
L.SetField(attrsTable.(*lua.LTable), attrName, lua.LString(attrValue))
return 0
}))
// Helper function to add a child node
L.SetGlobal("add_child", L.NewFunction(func(L *lua.LState) int {
parentTable := L.CheckTable(1)
childTable := L.CheckTable(2)
childrenTable := L.GetField(parentTable, "children")
if childrenTable == lua.LNil {
childrenTable = L.NewTable()
L.SetField(parentTable, "children", childrenTable)
}
childrenTbl := childrenTable.(*lua.LTable)
childrenTbl.RawSetInt(childrenTbl.Len()+1, childTable)
return 0
}))
}
// ProcessContent implements the Processor interface for XMLProcessor
// It processes XML content directly without file I/O
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) {
// Parse the XML document
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
// Parse XML document
doc, err := xmlquery.Parse(strings.NewReader(content))
if err != nil {
return "", 0, 0, fmt.Errorf("error parsing XML: %v", err)
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
}
// Find nodes matching XPath expression
// Find nodes matching the XPath pattern
nodes, err := xmlquery.QueryAll(doc, pattern)
if err != nil {
return "", 0, 0, fmt.Errorf("invalid XPath expression: %v", err)
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
}
// Log what we found
if p.Logger != nil {
p.Logger.Printf("XML mode selected with XPath expression: %s (found %d matching nodes)",
pattern, len(nodes))
}
if len(nodes) == 0 {
if p.Logger != nil {
p.Logger.Printf("No XML nodes matched XPath expression: %s", pattern)
}
matchCount := len(nodes)
if matchCount == 0 {
return content, 0, 0, nil
}
// Initialize Lua state
// Initialize Lua
L := lua.NewState()
defer L.Close()
// Setup Lua helper functions
if err := InitLuaHelpers(L); err != nil {
return "", 0, 0, err
// Load math library
L.Push(L.GetGlobal("require"))
L.Push(lua.LString("math"))
if err := L.PCall(1, 1, nil); err != nil {
return content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
}
// Register XML-specific helper functions
p.SetupXMLHelpers(L)
// Load helper functions
if err := InitLuaHelpers(L); err != nil {
return content, 0, 0, err
}
// Track modifications
matchCount := len(nodes)
modificationCount := 0
modifiedContent := content
modifications := []ModificationRecord{}
// Apply modifications to each node
modCount := 0
for _, node := range nodes {
// Reset Lua state for each node
L.SetGlobal("v1", lua.LNil)
L.SetGlobal("s1", lua.LNil)
// Process each matching node
for i, node := range nodes {
// Get the original text representation of this node
originalNodeText := p.XMLNodeToString(node)
if p.Logger != nil {
p.Logger.Printf("Found node #%d: %s", i+1, LimitString(originalNodeText, 100))
// Get the node value
var originalValue string
if node.Type == xmlquery.AttributeNode {
originalValue = node.InnerText()
} else if node.Type == xmlquery.TextNode {
originalValue = node.Data
} else {
originalValue = node.InnerText()
}
// For text nodes, we'll handle them directly
if node.Type == xmlquery.TextNode && node.Parent != nil {
// If this is a text node, we'll use its value directly
// Get the node's text content
textContent := node.Data
// Set up Lua environment
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric
L.SetGlobal("s1", lua.LString(textContent))
// Try to convert to number if possible
if floatVal, err := strconv.ParseFloat(textContent, 64); err == nil {
L.SetGlobal("v1", lua.LNumber(floatVal))
}
// Execute user's Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue // Skip this node on error
}
// Check for modifications
modVal := L.GetGlobal("v1")
if v, ok := modVal.(lua.LNumber); ok {
// If we have a numeric result, convert it to string
newValue := strconv.FormatFloat(float64(v), 'f', -1, 64)
if newValue != textContent {
// Replace the node content in the document
parentStr := p.XMLNodeToString(node.Parent)
newParentStr := strings.Replace(parentStr, textContent, newValue, 1)
modifiedContent = strings.Replace(modifiedContent, parentStr, newParentStr, 1)
modificationCount++
// Record the modification
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: textContent,
NewValue: newValue,
Operation: originalExpr,
Context: fmt.Sprintf("(XPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified text node #%d: '%s' -> '%s'",
i+1, LimitString(textContent, 30), LimitString(newValue, 30))
}
}
}
continue // Move to next node
// Convert to Lua variables
err = p.ToLua(L, originalValue)
if err != nil {
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
}
// Convert the node to a Lua table
nodeTable := p.NodeToLuaTable(L, node)
// Set the node in Lua global variable for user script
L.SetGlobal("node", nodeTable)
// Execute user's Lua script
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue // Skip this node on error
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
}
// Get modified node from Lua
modifiedNode, changed := p.GetModifiedNode(L, node)
if !changed {
if p.Logger != nil {
p.Logger.Printf("Node #%d was not modified by script", i+1)
}
// Get modified value
result, err := p.FromLua(L)
if err != nil {
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
}
newValue, ok := result.(string)
if !ok {
return content, modCount, matchCount, fmt.Errorf("expected string result from Lua, got %T", result)
}
// Skip if no change
if newValue == originalValue {
continue
}
// Render the modified node back to XML
modifiedNodeText := p.XMLNodeToString(modifiedNode)
// Replace just this node in the document
if originalNodeText != modifiedNodeText {
modifiedContent = strings.Replace(
modifiedContent,
originalNodeText,
modifiedNodeText,
1)
modificationCount++
// Record the modification for reporting
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: LimitString(originalNodeText, 30),
NewValue: LimitString(modifiedNodeText, 30),
Operation: originalExpr,
Context: fmt.Sprintf("(XPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified node #%d", i+1)
// Apply modification
if node.Type == xmlquery.AttributeNode {
// For attribute nodes, update the attribute value
node.Parent.Attr = append([]xmlquery.Attr{}, node.Parent.Attr...)
for i, attr := range node.Parent.Attr {
if attr.Name.Local == node.Data {
node.Parent.Attr[i].Value = newValue
break
}
}
} else if node.Type == xmlquery.TextNode {
// For text nodes, update the text content
node.Data = newValue
} else {
// For element nodes, replace inner text
// Simple approach: set the InnerText directly if there are no child elements
if node.FirstChild == nil || (node.FirstChild != nil && node.FirstChild.Type == xmlquery.TextNode && node.FirstChild.NextSibling == nil) {
if node.FirstChild != nil {
node.FirstChild.Data = newValue
} else {
// Create a new text node and add it as the first child
textNode := &xmlquery.Node{
Type: xmlquery.TextNode,
Data: newValue,
}
node.FirstChild = textNode
}
} else {
// Complex case: node has mixed content or child elements
// Replace just the text content while preserving child elements
// This is a simplified approach - more complex XML may need more robust handling
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == xmlquery.TextNode {
child.Data = newValue
break // Update only the first text node
}
}
}
}
modCount++
}
// Serialize the modified XML document to string
if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode {
// If we have an XML declaration, start with it
declaration := doc.FirstChild.OutputXML(true)
// Remove the firstChild (declaration) before serializing the rest of the document
doc.FirstChild = doc.FirstChild.NextSibling
return declaration + doc.OutputXML(true), modCount, matchCount, nil
}
return doc.OutputXML(true), modCount, matchCount, nil
}
// ToLua converts XML node values to Lua variables
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
value, ok := data.(string)
if !ok {
return fmt.Errorf("expected string value, got %T", data)
}
// Set as string variable
L.SetGlobal("s1", lua.LString(value))
// Try to convert to number if possible
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0
if err := L.DoString(fmt.Sprintf("v1 = tonumber(%q) or 0", value)); err != nil {
return fmt.Errorf("error converting value to number: %v", err)
}
return nil
}
// FromLua gets modified values from Lua
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Check if string variable was modified
s1 := L.GetGlobal("s1")
if s1 != lua.LNil {
if s1Str, ok := s1.(lua.LString); ok {
return string(s1Str), nil
}
}
if p.Logger != nil && modificationCount > 0 {
p.Logger.Printf("Made %d XML node modifications", modificationCount)
// Check if numeric variable was modified
v1 := L.GetGlobal("v1")
if v1 != lua.LNil {
if v1Num, ok := v1.(lua.LNumber); ok {
return fmt.Sprintf("%v", v1Num), nil
}
}
return modifiedContent, modificationCount, matchCount, nil
// Default return empty string
return "", nil
}

File diff suppressed because it is too large Load Diff