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 ( 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/antchfx/xpath v1.3.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/net v0.33.0 // indirect
golang.org/x/text v0.21.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 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc= 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 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= 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 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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 h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 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/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/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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= 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=

272
main.go
View File

@@ -3,11 +3,8 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"regexp"
"strings"
"sync" "sync"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
@@ -15,66 +12,37 @@ import (
"modify/processor" "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 { type GlobalStats struct {
TotalMatches int TotalMatches int
TotalModifications int TotalModifications int
Modifications []processor.ModificationRecord
ProcessedFiles int ProcessedFiles int
FailedFiles int FailedFiles int
} }
// FileMode defines how we interpret and process files
type FileMode string type FileMode string
const ( const (
ModeRegex FileMode = "regex" // Default mode using regex ModeRegex FileMode = "regex"
ModeXML FileMode = "xml" // XML mode using XPath ModeXML FileMode = "xml"
ModeJSON FileMode = "json" // JSON mode using JSONPath ModeJSON FileMode = "json"
) )
var stats GlobalStats 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() { func init() {
// Configure standard logging to be hidden by default
log.SetFlags(log.Lmicroseconds | log.Lshortfile) 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 stats = GlobalStats{}
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),
}
} }
func main() { 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() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nOptions:\n") fmt.Fprintf(os.Stderr, "\nOptions:\n")
@@ -103,85 +71,64 @@ func main() {
} }
flag.Parse() flag.Parse()
// Set up verbose mode
if !*verboseFlag {
// If not verbose, suppress Info level logs
Info.SetOutput(io.Discard)
}
args := flag.Args() args := flag.Args()
requiredArgCount := 3 // Default for regex mode
// XML/JSON modes need one fewer positional argument if len(args) < 3 {
if *fileModeFlag == "xml" || *fileModeFlag == "json" { fmt.Fprintf(os.Stderr, "%s mode requires %d arguments minimum\n", *fileModeFlag, 3)
requiredArgCount = 2
}
if len(args) < requiredArgCount {
Error.Printf("%s mode requires %d arguments minimum", *fileModeFlag, requiredArgCount)
flag.Usage() flag.Usage()
return 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 // Get the appropriate pattern and expression based on mode
var regexPattern string var pattern, luaExpr string
var luaExpr string
var filePatterns []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" { if *fileModeFlag == "regex" {
regexPattern = args[0] pattern = args[0]
luaExpr = args[1] luaExpr = args[1]
filePatterns = args[2:] filePatterns = args[2:]
// Process files with regex mode
processFilesWithRegex(regexPattern, luaExpr, filePatterns)
} else { } else {
// XML/JSON modes // For XML/JSON modes, pattern comes from flags
luaExpr = args[0] luaExpr = args[0]
filePatterns = args[1:] filePatterns = args[1:]
}
// Prepare the Lua expression // Prepare the Lua expression
originalLuaExpr := luaExpr originalLuaExpr := luaExpr
luaExpr = processor.BuildLuaScript(luaExpr) luaExpr = processor.BuildLuaScript(luaExpr)
if originalLuaExpr != luaExpr { if originalLuaExpr != luaExpr {
Info.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr) logger.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr)
} }
// Expand file patterns with glob support // Expand file patterns with glob support
files, err := expandFilePatterns(filePatterns) files, err := expandFilePatterns(filePatterns)
if err != nil { if err != nil {
Error.Printf("Error expanding file patterns: %v", err) fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
return return
} }
if len(files) == 0 { 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 return
} }
// Create the processor based on mode // Create the processor based on mode
var proc processor.Processor var proc processor.Processor
if *fileModeFlag == "xml" { switch *fileModeFlag {
Info.Printf("Starting XML modifier with XPath '%s', expression '%s' on %d files", case "regex":
*xpathFlag, luaExpr, len(files)) proc = &processor.RegexProcessor{}
proc = processor.NewXMLProcessor(Info) logger.Printf("Starting regex modifier with pattern '%s', expression '%s' on %d files",
} else { pattern, luaExpr, len(files))
Info.Printf("Starting JSON modifier with JSONPath '%s', expression '%s' on %d files", // case "xml":
*jsonpathFlag, luaExpr, len(files)) // proc = &processor.XMLProcessor{}
proc = processor.NewJSONProcessor(Info) // 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 var wg sync.WaitGroup
@@ -190,22 +137,14 @@ func main() {
wg.Add(1) wg.Add(1)
go func(file string) { go func(file string) {
defer wg.Done() defer wg.Done()
Info.Printf("🔄 Processing file: %s", file) logger.Printf("Processing file: %s", file)
// Pass the appropriate path expression as the pattern modCount, matchCount, err := proc.Process(file, pattern, luaExpr)
var pattern string
if *fileModeFlag == "xml" {
pattern = *xpathFlag
} else {
pattern = *jsonpathFlag
}
modCount, matchCount, err := proc.Process(file, pattern, luaExpr, originalLuaExpr)
if err != nil { 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++ stats.FailedFiles++
} else { } else {
Info.Printf("Successfully processed file: %s", file) logger.Printf("Successfully processed file: %s", file)
stats.ProcessedFiles++ stats.ProcessedFiles++
stats.TotalMatches += matchCount stats.TotalMatches += matchCount
stats.TotalModifications += modCount stats.TotalModifications += modCount
@@ -213,138 +152,15 @@ func main() {
}(file) }(file)
} }
wg.Wait() wg.Wait()
}
// Print summary of all modifications // Print summary
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
}
// 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
}
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)
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)
modCount, matchCount, err := proc.Process(file, regexPattern, 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()
}
// printSummary outputs a formatted summary of all modifications made
func printSummary(operation string) {
if stats.TotalModifications == 0 { if stats.TotalModifications == 0 {
Warning.Printf("No modifications were made in any files") fmt.Fprintf(os.Stderr, "No modifications were made in any files\n")
return
}
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 { } else {
for i, mod := range mods { fmt.Printf("Operation complete! Modified %d values in %d/%d files\n",
Success.Printf(" %d. '%s' → '%s' %s", stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
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) { func expandFilePatterns(patterns []string) ([]string, error) {
var files []string var files []string
@@ -360,7 +176,7 @@ func expandFilePatterns(patterns []string) ([]string, error) {
} }
if len(files) > 0 { 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 return files, nil
} }

View File

@@ -5,30 +5,18 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/PaesslerAG/jsonpath"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
// JSONProcessor implements the Processor interface using JSONPath // JSONProcessor implements the Processor interface for JSON documents
type JSONProcessor struct { type JSONProcessor struct{}
Logger Logger
}
// NewJSONProcessor creates a new JSONProcessor
func NewJSONProcessor(logger Logger) *JSONProcessor {
return &JSONProcessor{
Logger: logger,
}
}
// Process implements the Processor interface for JSONProcessor // Process implements the Processor interface for JSONProcessor
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) { func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
// Use pattern as JSONPath expression
jsonPathExpr := pattern
// Read file content // Read file content
fullPath := filepath.Join(".", filename) fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
@@ -37,12 +25,9 @@ func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string,
} }
fileContent := string(content) fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the 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 { if err != nil {
return 0, 0, err return 0, 0, err
} }
@@ -53,557 +38,271 @@ func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string,
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err) 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 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 // 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) (string, int, int, error) {
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) { // Parse JSON document
// Parse JSON var jsonData interface{}
var jsonDoc interface{} err := json.Unmarshal([]byte(content), &jsonData)
err := json.Unmarshal([]byte(content), &jsonDoc)
if err != nil { 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 // Find nodes matching the JSONPath pattern
if p.Logger != nil { paths, values, err := p.findJSONPaths(jsonData, pattern)
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)
if err != nil { 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 { matchCount := len(paths)
if p.Logger != nil { if matchCount == 0 {
p.Logger.Printf("No JSON nodes matched JSONPath expression: %s", pattern)
}
return content, 0, 0, nil return content, 0, 0, nil
} }
if p.Logger != nil { // Initialize Lua
p.Logger.Printf("Found %d JSON nodes matching the path", len(matchingPaths)) 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 // Load helper functions
matchCount := len(matchingPaths) if err := InitLuaHelpers(L); err != nil {
modificationCount := 0 return content, 0, 0, err
modifications := []ModificationRecord{} }
// Clone the document for modification // Apply modifications to each node
var modifiedDoc interface{} modCount := 0
modifiedBytes, err := json.Marshal(jsonDoc) for i, value := range values {
// Reset Lua state for each node
L.SetGlobal("v1", lua.LNil)
L.SetGlobal("s1", lua.LNil)
// Convert to Lua variables
err = p.ToLua(L, value)
if err != nil { if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err) return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
} }
err = json.Unmarshal(modifiedBytes, &modifiedDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
}
// 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 // Execute Lua script
if err := L.DoString(luaExpr); err != nil { if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil { return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue
} }
// Extract modified value // Get modified value
modVal := L.GetGlobal("v1") result, err := p.FromLua(L)
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 err != nil {
if p.Logger != nil { return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
p.Logger.Printf("Error updating value at path %v: %v", path, err)
} }
// Skip if value didn't change
if fmt.Sprintf("%v", value) == fmt.Sprintf("%v", result) {
continue continue
} }
modificationCount++ // Apply the modification to the JSON data
modifications = append(modifications, ModificationRecord{ err = p.updateJSONValue(jsonData, paths[i], result)
File: "", if err != nil {
OldValue: fmt.Sprintf("%v", val), return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err)
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)
} }
modCount++
}
// 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
}
// 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
}
// Convert values to a slice if it's not already
valuesSlice := []interface{}{}
paths := []string{}
switch v := values.(type) {
case []interface{}:
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)
}
}
default:
valuesSlice = append(valuesSlice, v)
paths = append(paths, pattern)
}
return paths, valuesSlice, nil
}
// 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
// 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)
}
// 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 {
// Handle object property
obj, _ := current.(map[string]interface{})
obj[part] = newValue
}
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]
} }
} }
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: case string:
// Set up Lua environment for string value L.SetGlobal("s1", lua.LString(v))
L.SetGlobal("s1", lua.LString(val))
// Try to convert to number if possible // Try to convert to number if possible
if floatVal, err := strconv.ParseFloat(val, 64); err == nil { if val, err := strconv.ParseFloat(v, 64); err == nil {
L.SetGlobal("v1", lua.LNumber(floatVal)) L.SetGlobal("v1", lua.LNumber(val))
} else { } else {
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric L.SetGlobal("v1", lua.LNumber(0))
} }
// 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, "", " ")
if err != nil {
return "", 0, 0, fmt.Errorf("error marshaling modified JSON: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d JSON node modifications", modificationCount)
}
return string(modifiedJSON), modificationCount, matchCount, nil
}
// 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
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
}
// 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
}
return current, 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")
}
// The last element in the path is the value itself
return path[len(path)-1], nil
}
// 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: case bool:
return lua.LBool(v) if v {
case float64: L.SetGlobal("v1", lua.LNumber(1))
return lua.LNumber(v) } else {
case string: L.SetGlobal("v1", lua.LNumber(0))
return lua.LString(v)
case []interface{}:
arr := L.NewTable()
for i, item := range v {
arr.RawSetInt(i+1, p.jsonToLua(L, item))
} }
return arr L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
case map[string]interface{}:
obj := L.NewTable()
for k, item := range v {
obj.RawSetString(k, p.jsonToLua(L, item))
}
return obj
default: default:
// For unknown types, convert to string representation // For complex types, convert to string
return lua.LString(fmt.Sprintf("%v", val)) L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", v)))
L.SetGlobal("v1", lua.LNumber(0))
} }
}
// 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 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)
// Check if it's an array or an object // FromLua retrieves values from Lua
isArray := true func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
maxN := 0 // 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
}
}
table.ForEach(func(key, _ lua.LValue) { // Check if numeric variable was modified
if key.Type() == lua.LTNumber { v1 := L.GetGlobal("v1")
n := int(key.(lua.LNumber)) if v1 != lua.LNil {
if n > maxN { if v1Num, ok := v1.(lua.LNumber); ok {
maxN = n return float64(v1Num), nil
} }
} else {
isArray = false
} }
})
if isArray && maxN > 0 { // Default return nil
// It's an array return nil, nil
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
}
default:
// For functions, userdata, etc., convert to string
return val.String()
}
} }

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 // Processor defines the interface for all file processors
type Processor interface { type Processor interface {
// Process handles processing a file with the given pattern and Lua expression // 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 // 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 // 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 converts processor-specific data to Lua variables
ToLua(L *lua.LState, data interface{}) error ToLua(L *lua.LState, data interface{}) error
@@ -76,6 +76,29 @@ func LimitString(s string, maxLen int) string {
return s[:maxLen-3] + "..." 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 // Max returns the maximum of two integers
func Max(a, b int) int { func Max(a, b int) int {
if a > b { if a > b {

View File

@@ -12,26 +12,10 @@ import (
) )
// RegexProcessor implements the Processor interface using regex patterns // RegexProcessor implements the Processor interface using regex patterns
type RegexProcessor struct { 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,
}
}
// Process implements the Processor interface for RegexProcessor // 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 // Read file content
fullPath := filepath.Join(".", filename) fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
@@ -40,32 +24,19 @@ func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string
} }
fileContent := string(content) fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the content with regex // Process the content
result, modCount, matchCount, err := p.ProcessContent(fileContent, luaExpr, filename, originalExpr) modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
if modCount == 0 { // If we made modifications, save the file
if p.Logger != nil { if modCount > 0 {
p.Logger.Printf("No modifications made to %s - pattern didn't match any content", fullPath) err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
}
return 0, 0, nil
}
// Write the modified content back
err = os.WriteFile(fullPath, []byte(result), 0644)
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err) 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 return modCount, matchCount, nil
@@ -109,14 +80,22 @@ func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
vLuaVal := L.GetGlobal(vVarName) vLuaVal := L.GetGlobal(vVarName)
sLuaVal := L.GetGlobal(sVarName) 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 { if vLuaVal != lua.LNil {
switch v := vLuaVal.(type) { switch v := vLuaVal.(type) {
case lua.LNumber: case lua.LNumber:
// Convert numeric value to string // Convert numeric value to string
newNumVal := strconv.FormatFloat(float64(v), 'f', -1, 64) newNumVal := strconv.FormatFloat(float64(v), 'f', -1, 64)
modifications[i] = newNumVal modifications[i] = newNumVal
// We found a value, continue to next capture group
continue continue
case lua.LString: case lua.LString:
// Use string value directly // Use string value directly
@@ -130,113 +109,70 @@ func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
continue 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 return modifications, nil
} }
// ProcessContent applies regex replacement with Lua processing // 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() L := lua.NewState()
defer L.Close() defer L.Close()
// Initialize Lua environment // Initialize Lua environment
modificationCount := 0 modificationCount := 0
matchCount := 0 matchCount := 0
modifications := []ModificationRecord{}
// Load math library // Load math library
L.Push(L.GetGlobal("require")) L.Push(L.GetGlobal("require"))
L.Push(lua.LString("math")) L.Push(lua.LString("math"))
if err := L.PCall(1, 1, nil); err != nil { if err := L.PCall(1, 1, nil); err != nil {
if p.Logger != nil { return content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
p.Logger.Printf("Failed to load Lua math library: %v", err)
}
return data, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
} }
// Initialize helper functions // Initialize helper functions
if err := InitLuaHelpers(L); err != nil { if err := InitLuaHelpers(L); err != nil {
return data, 0, 0, err return content, 0, 0, err
} }
// Process all regex matches // Process all regex matches
result := p.CompiledPattern.ReplaceAllStringFunc(data, func(match string) string { result := compiledPattern.ReplaceAllStringFunc(content, func(match string) string {
matchCount++ matchCount++
captures := p.CompiledPattern.FindStringSubmatch(match) captures := compiledPattern.FindStringSubmatch(match)
if len(captures) <= 1 { if len(captures) <= 1 {
// No capture groups, return unchanged // No capture groups, return unchanged
if p.Logger != nil {
p.Logger.Printf("Match found but no capture groups: %s", LimitString(match, 50))
}
return match return match
} }
if p.Logger != nil {
p.Logger.Printf("Match found: %s", LimitString(match, 50))
}
// Pass the captures to Lua environment // Pass the captures to Lua environment
if err := p.ToLua(L, captures); err != nil { if err := p.ToLua(L, captures); err != nil {
if p.Logger != nil {
p.Logger.Printf("Failed to set Lua variables: %v", err)
}
return match 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 // Execute the user's Lua code
if err := L.DoString(luaExpr); err != nil { 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 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 // Get modifications from Lua
modResult, err := p.FromLua(L) modResult, err := p.FromLua(L)
if err != nil { if err != nil {
if p.Logger != nil {
p.Logger.Printf("Failed to get modifications from Lua: %v", err)
}
return match return match
} }
// Debug: print the modifications detected
if p.Logger != nil {
p.Logger.Printf("Modifications detected: %v", modResult)
}
// Apply modifications to the matched text // Apply modifications to the matched text
modsMap, ok := modResult.(map[int]string) modsMap, ok := modResult.(map[int]string)
if !ok || len(modsMap) == 0 { if !ok || len(modsMap) == 0 {
p.Logger.Printf("No modifications detected after Lua script execution")
return match // No changes return match // No changes
} }
@@ -244,57 +180,7 @@ func (p *RegexProcessor) ProcessContent(data string, luaExpr string, filename st
result := match result := match
for i, newVal := range modsMap { for i, newVal := range modsMap {
oldVal := captures[i+1] 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) 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)),
})
} }
modificationCount++ modificationCount++
@@ -303,26 +189,3 @@ func (p *RegexProcessor) ProcessContent(data string, luaExpr string, filename st
return result, modificationCount, matchCount, nil 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), " ") 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) { func TestSimpleValueMultiplication(t *testing.T) {
content := ` content := `<config>
<config>
<item> <item>
<value>100</value> <value>100</value>
</item> </item>
</config> </config>`
`
expected := ` expected := `<config>
<config>
<item> <item>
<value>150</value> <value>150</value>
</item> </item>
</config> </config>`
`
// Create a regex pattern with the (?s) flag for multiline matching p := &RegexProcessor{}
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+)</value>`, "v1 = v1*1.5")
processor := NewRegexProcessor(regex, &TestLogger{T: t})
luaExpr := BuildLuaScript("*1.5")
// 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)
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "*1.5")
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 1 {
if matchCount != 1 { t.Errorf("Expected 1 match, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
t.Logf("Modified content: %s", modifiedContent) if mods != 1 {
t.Logf("Expected content: %s", expected) t.Errorf("Expected 1 modification, got %d", mods)
}
// Compare normalized content if result != expected {
normalizedModified := normalizeWhitespace(modifiedContent) t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
normalizedExpected := normalizeWhitespace(expected)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
} }
} }
func TestShorthandNotation(t *testing.T) { func TestShorthandNotation(t *testing.T) {
content := ` content := `<config>
<config>
<item> <item>
<value>100</value> <value>100</value>
</item> </item>
</config> </config>`
`
expected := ` expected := `<config>
<config>
<item> <item>
<value>150</value> <value>150</value>
</item> </item>
</config> </config>`
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) p := &RegexProcessor{}
processor := NewRegexProcessor(regex, &TestLogger{}) result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+)</value>`, "v1*1.5")
luaExpr := BuildLuaScript("v1 * 1.5") // Use direct assignment syntax
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 * 1.5")
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 1 {
if matchCount != 1 { t.Errorf("Expected 1 match, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 1 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 1 modification, got %d", mods)
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 TestShorthandNotationFloats(t *testing.T) { func TestShorthandNotationFloats(t *testing.T) {
content := ` content := `<config>
<config>
<item> <item>
<value>132.671327</value> <value>10.5</value>
</item> </item>
</config> </config>`
`
expected := ` expected := `<config>
<config>
<item> <item>
<value>176.01681007940928</value> <value>15.75</value>
</item> </item>
</config> </config>`
`
regex := regexp.MustCompile(`(?s)<value>(\d*\.?\d+)</value>`) p := &RegexProcessor{}
processor := NewRegexProcessor(regex, &TestLogger{}) result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(\d+\.\d+)</value>`, "v1*1.5")
luaExpr := BuildLuaScript("v1 * 1.32671327") // Use direct assignment syntax
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 * 1.32671327")
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 1 {
if matchCount != 1 { t.Errorf("Expected 1 match, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 1 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 1 modification, got %d", mods)
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 TestArrayNotation(t *testing.T) { func TestArrayNotation(t *testing.T) {
content := ` content := `<config>
<config> <prices>
<item> <price>10</price>
<value>100</value> <price>20</price>
</item> <price>30</price>
</config> </prices>
` </config>`
expected := `
<config>
<item>
<value>150</value>
</item>
</config>
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) expected := `<config>
processor := NewRegexProcessor(regex, &TestLogger{}) <prices>
luaExpr := BuildLuaScript("v1 = v1 * 1.5") // Use direct assignment syntax <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 { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 3 {
if matchCount != 1 { t.Errorf("Expected 3 matches, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 3 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 3 modifications, got %d", mods)
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 TestMultipleMatches(t *testing.T) { func TestMultipleMatches(t *testing.T) {
content := ` content := `<data>
<config> <entry>50</entry>
<item> <entry>100</entry>
<value>100</value> <entry>200</entry>
</item> </data>`
<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>
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) expected := `<data>
processor := NewRegexProcessor(regex, &TestLogger{}) <entry>100</entry>
luaExpr := BuildLuaScript("*1.5") <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 { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 3 {
if matchCount != 3 { t.Errorf("Expected 3 matches, got %d", matches)
t.Errorf("Expected 3 matches, got %d", matchCount)
}
if modCount != 3 {
t.Errorf("Expected 3 modifications, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 3 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 3 modifications, got %d", mods)
if normalizedModified != normalizedExpected {
t.Fatalf("Expected modified content to be %q, but got %q", normalizedExpected, normalizedModified)
}
} }
func TestMultipleCaptureGroups(t *testing.T) { if result != expected {
content := ` t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
<config>
<item>
<value>10</value>
<multiplier>5</multiplier>
</item>
</config>
`
expected := `
<config>
<item>
<value>50</value>
<multiplier>5</multiplier>
</item>
</config>
`
// 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
// 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)
} }
modifiedContent, modCount, matchCount, err := processor.ProcessContent(content, luaExpr, "test", "v1 = v1 * v2") // 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 { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 2 {
if matchCount != 1 { t.Errorf("Expected 2 matches, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 2 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 2 modifications, got %d", mods)
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 TestModifyingMultipleValues(t *testing.T) { func TestStringOperations(t *testing.T) {
content := ` content := `<users>
<config> <user>John</user>
<item> <user>Mary</user>
<value>50</value> </users>`
<multiplier>3</multiplier>
<divider>2</divider>
</item>
</config>
`
expected := `
<config>
<item>
<value>75</value>
<multiplier>5</multiplier>
<divider>1</divider>
</item>
</config>
`
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>.*?<multiplier>(\d+)</multiplier>.*?<divider>(\d+)</divider>`) expected := `<users>
processor := NewRegexProcessor(regex, &TestLogger{}) <user>JOHN</user>
luaExpr := BuildLuaScript("v1 = v1 * v2 / v3; v2 = min(v2 * 2, 5); v3 = max(1, v3 / 2)") <user>MARY</user>
</users>`
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 / v3; v2 = min(v2 * 2, 5); v3 = max(1, v3 / 2)")
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
// Verify match and modification counts if matches != 2 {
if matchCount != 1 { t.Errorf("Expected 2 matches, got %d", matches)
t.Errorf("Expected 1 match, got %d", matchCount)
}
if modCount != 1 {
t.Errorf("Expected 1 modification, got %d", modCount)
} }
normalizedModified := normalizeWhitespace(modifiedContent) if mods != 2 {
normalizedExpected := normalizeWhitespace(expected) t.Errorf("Expected 2 modifications, got %d", mods)
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)
}
// 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"`)
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)
} }
} }
@@ -356,10 +324,10 @@ func TestDecimalValues(t *testing.T) {
` `
regex := regexp.MustCompile(`(?s)<value>([0-9.]+)</value>.*?<multiplier>([0-9.]+)</multiplier>`) regex := regexp.MustCompile(`(?s)<value>([0-9.]+)</value>.*?<multiplier>([0-9.]+)</multiplier>`)
processor := NewRegexProcessor(regex, &TestLogger{}) p := &RegexProcessor{}
luaExpr := BuildLuaScript("v1 = v1 * v2") 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 { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
@@ -389,10 +357,10 @@ func TestLuaMathFunctions(t *testing.T) {
` `
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{}) p := &RegexProcessor{}
luaExpr := BuildLuaScript("v1 = math.sqrt(v1)") 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 { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
@@ -422,10 +390,10 @@ func TestDirectAssignment(t *testing.T) {
` `
regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`) regex := regexp.MustCompile(`(?s)<value>(\d+)</value>`)
processor := NewRegexProcessor(regex, &TestLogger{}) p := &RegexProcessor{}
luaExpr := BuildLuaScript("=0") luaExpr := BuildLuaScript("=0")
modifiedContent, _, _, err := processor.ProcessContent(content, luaExpr, "test", "=0") modifiedContent, _, _, err := p.ProcessContent(content, regex.String(), luaExpr)
if err != nil { if err != nil {
t.Fatalf("Error processing content: %v", err) t.Fatalf("Error processing content: %v", err)
} }
@@ -457,10 +425,10 @@ func TestStringAndNumericOperations(t *testing.T) {
}, },
{ {
name: "Basic string manipulation", name: "Basic string manipulation",
input: "<n>test</n>", input: "<name>test</name>",
regexPattern: "<n>(.*?)</n>", regexPattern: "<name>(.*?)</name>",
luaExpression: "s1 = string.upper(s1)", luaExpression: "s1 = string.upper(s1)",
expectedOutput: "<n>TEST</n>", expectedOutput: "<name>TEST</name>",
expectedMods: 1, expectedMods: 1,
}, },
{ {
@@ -484,12 +452,12 @@ func TestStringAndNumericOperations(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Compile the regex pattern with multiline support // Compile the regex pattern with multiline support
pattern := regexp.MustCompile("(?s)" + tt.regexPattern) pattern := "(?s)" + tt.regexPattern
processor := NewRegexProcessor(pattern, &TestLogger{}) p := &RegexProcessor{}
luaExpr := BuildLuaScript(tt.luaExpression) luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function // 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 { if err != nil {
t.Fatalf("Process function failed: %v", err) t.Fatalf("Process function failed: %v", err)
} }
@@ -553,12 +521,12 @@ func TestEdgeCases(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Make sure the regex can match across multiple lines // Make sure the regex can match across multiple lines
pattern := regexp.MustCompile("(?s)" + tt.regexPattern) pattern := "(?s)" + tt.regexPattern
processor := NewRegexProcessor(pattern, &TestLogger{}) p := &RegexProcessor{}
luaExpr := BuildLuaScript(tt.luaExpression) luaExpr := BuildLuaScript(tt.luaExpression)
// Process with our function // 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 { if err != nil {
t.Fatalf("Process function failed: %v", err) 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" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/antchfx/xmlquery" "github.com/antchfx/xmlquery"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
// XMLProcessor implements the Processor interface using XPath // XMLProcessor implements the Processor interface for XML documents
type XMLProcessor struct { type XMLProcessor struct{}
Logger Logger
}
// NewXMLProcessor creates a new XMLProcessor
func NewXMLProcessor(logger Logger) *XMLProcessor {
return &XMLProcessor{
Logger: logger,
}
}
// Process implements the Processor interface for XMLProcessor // Process implements the Processor interface for XMLProcessor
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) { func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
// Use pattern as XPath expression
xpathExpr := pattern
// Read file content // Read file content
fullPath := filepath.Join(".", filename) fullPath := filepath.Join(".", filename)
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
@@ -36,12 +23,9 @@ func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string,
} }
fileContent := string(content) fileContent := string(content)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the 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 { if err != nil {
return 0, 0, err return 0, 0, err
} }
@@ -52,403 +36,182 @@ func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string,
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("error writing file: %v", err) 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 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 // 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) (string, int, int, error) {
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) { // Parse XML document
// Parse the XML document
doc, err := xmlquery.Parse(strings.NewReader(content)) doc, err := xmlquery.Parse(strings.NewReader(content))
if err != nil { 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) nodes, err := xmlquery.QueryAll(doc, pattern)
if err != nil { 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 matchCount := len(nodes)
if p.Logger != nil { if matchCount == 0 {
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)
}
return content, 0, 0, nil return content, 0, 0, nil
} }
// Initialize Lua state // Initialize Lua
L := lua.NewState() L := lua.NewState()
defer L.Close() defer L.Close()
// Setup Lua helper functions // 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)
}
// Load helper functions
if err := InitLuaHelpers(L); err != nil { if err := InitLuaHelpers(L); err != nil {
return "", 0, 0, err return content, 0, 0, err
} }
// Register XML-specific helper functions // Apply modifications to each node
p.SetupXMLHelpers(L) modCount := 0
for _, node := range nodes {
// Reset Lua state for each node
L.SetGlobal("v1", lua.LNil)
L.SetGlobal("s1", lua.LNil)
// Track modifications // Get the node value
matchCount := len(nodes) var originalValue string
modificationCount := 0 if node.Type == xmlquery.AttributeNode {
modifiedContent := content originalValue = node.InnerText()
modifications := []ModificationRecord{} } else if node.Type == xmlquery.TextNode {
originalValue = node.Data
// Process each matching node } else {
for i, node := range nodes { originalValue = node.InnerText()
// 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))
} }
// For text nodes, we'll handle them directly // Convert to Lua variables
if node.Type == xmlquery.TextNode && node.Parent != nil { err = p.ToLua(L, originalValue)
// If this is a text node, we'll use its value directly if err != nil {
// Get the node's text content return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
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 // Execute Lua script
if err := L.DoString(luaExpr); err != nil { if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil { return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue // Skip this node on error
} }
// Check for modifications // Get modified value
modVal := L.GetGlobal("v1") result, err := p.FromLua(L)
if v, ok := modVal.(lua.LNumber); ok { if err != nil {
// If we have a numeric result, convert it to string return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
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 the node to a Lua table newValue, ok := result.(string)
nodeTable := p.NodeToLuaTable(L, node) if !ok {
return content, modCount, matchCount, fmt.Errorf("expected string result from Lua, got %T", result)
// Set the node in Lua global variable for user script
L.SetGlobal("node", nodeTable)
// 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
} }
// Get modified node from Lua // Skip if no change
modifiedNode, changed := p.GetModifiedNode(L, node) if newValue == originalValue {
if !changed {
if p.Logger != nil {
p.Logger.Printf("Node #%d was not modified by script", i+1)
}
continue continue
} }
// Render the modified node back to XML // Apply modification
modifiedNodeText := p.XMLNodeToString(modifiedNode) if node.Type == xmlquery.AttributeNode {
// For attribute nodes, update the attribute value
// Replace just this node in the document node.Parent.Attr = append([]xmlquery.Attr{}, node.Parent.Attr...)
if originalNodeText != modifiedNodeText { for i, attr := range node.Parent.Attr {
modifiedContent = strings.Replace( if attr.Name.Local == node.Data {
modifiedContent, node.Parent.Attr[i].Value = newValue
originalNodeText, break
modifiedNodeText, }
1) }
modificationCount++ } else if node.Type == xmlquery.TextNode {
// For text nodes, update the text content
// Record the modification for reporting node.Data = newValue
modifications = append(modifications, ModificationRecord{ } else {
File: "", // For element nodes, replace inner text
OldValue: LimitString(originalNodeText, 30), // Simple approach: set the InnerText directly if there are no child elements
NewValue: LimitString(modifiedNodeText, 30), if node.FirstChild == nil || (node.FirstChild != nil && node.FirstChild.Type == xmlquery.TextNode && node.FirstChild.NextSibling == nil) {
Operation: originalExpr, if node.FirstChild != nil {
Context: fmt.Sprintf("(XPath: %s)", pattern), node.FirstChild.Data = newValue
}) } else {
// Create a new text node and add it as the first child
if p.Logger != nil { textNode := &xmlquery.Node{
p.Logger.Printf("Modified node #%d", i+1) 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
}
} }
} }
} }
if p.Logger != nil && modificationCount > 0 { modCount++
p.Logger.Printf("Made %d XML node modifications", modificationCount)
} }
return modifiedContent, modificationCount, matchCount, nil // 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
}
}
// 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
}
}
// Default return empty string
return "", nil
} }

File diff suppressed because it is too large Load Diff