Integrate the xml processing with the rest of the project

This commit is contained in:
2025-12-19 11:44:07 +01:00
parent f1ea0f9156
commit 74394cbde9
4 changed files with 380 additions and 22 deletions

View File

@@ -457,8 +457,10 @@ STRING FUNCTIONS:
format(s, ...) - Formats string using Lua string.format
trim(s) - Removes leading/trailing whitespace
strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
fromCSV(csv, delimiter, hasHeaders) - Parses CSV text into rows of fields (delimiter defaults to ",", hasHeaders defaults to false)
toCSV(rows, delimiter) - Converts table of rows to CSV text format (delimiter defaults to ",")
fromCSV(csv, options) - Parses CSV text into rows of fields
options: {delimiter=",", hasheader=false, hascomments=false}
toCSV(rows, options) - Converts table of rows to CSV text format
options: {delimiter=",", hasheader=false}
num(str) - Converts string to number (returns 0 if invalid)
str(num) - Converts number to string
is_number(str) - Returns true if string is numeric
@@ -467,6 +469,31 @@ TABLE FUNCTIONS:
dump(table, depth) - Prints table structure recursively
isArray(t) - Returns true if table is a sequential array
XML HELPER FUNCTIONS:
findElements(root, tagName) - Find all elements with specific tag name
visitElements(root, callback) - Visit all elements recursively
callback(element, depth, path)
filterElements(root, predicate) - Find elements matching condition
predicate(element) returns true/false
getNumAttr(element, attrName) - Get numeric attribute value
setNumAttr(element, attrName, value) - Set numeric attribute value
modifyNumAttr(element, attrName, func)- Modify numeric attribute with function
func(currentValue) returns newValue
hasAttr(element, attrName) - Check if attribute exists
getAttr(element, attrName) - Get attribute value as string
setAttr(element, attrName, value) - Set attribute value
getText(element) - Get element text content
setText(element, text) - Set element text content
JSON HELPER FUNCTIONS:
visitJSON(data, callback) - Visit all values in JSON structure
callback(value, key, parent)
findInJSON(data, predicate) - Find values matching condition
predicate(value, key, parent) returns true/false
modifyJSONNumbers(data, predicate, modifier) - Modify numeric values
predicate(value, key, parent) returns true/false
modifier(currentValue) returns newValue
HTTP FUNCTIONS:
fetch(url, options) - Makes HTTP request, returns response table
options: {method="GET", headers={}, body=""}
@@ -480,12 +507,31 @@ UTILITY FUNCTIONS:
print(...) - Prints arguments to Go logger
EXAMPLES:
-- Math
round(3.14159, 2) -> 3.14
min(5, 3) -> 3
-- String
strsplit("a,b,c", ",") -> {"a", "b", "c"}
upper("hello") -> "HELLO"
min(5, 3) -> 3
num("123") -> 123
is_number("abc") -> false
fetch("https://api.example.com/data")
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
-- XML (where root is XML element with _tag, _attr, _children fields)
local items = findElements(root, "Item")
for _, item in ipairs(items) do
modifyNumAttr(item, "Weight", function(w) return w * 2 end)
end
-- JSON (where data is parsed JSON object)
visitJSON(data, function(value, key, parent)
if type(value) == "number" and key == "price" then
parent[key] = value * 1.5
end
end)
-- HTTP
local response = fetch("https://api.example.com/data")
if response.ok then
print(response.body)
end`
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
lua "github.com/yuin/gopher-lua"
)
var xmlLogger = logger.Default.WithPrefix("processor/xml")
@@ -445,3 +446,149 @@ func formatNumeric(f float64) string {
}
return strconv.FormatFloat(f, 'f', -1, 64)
}
// ProcessXML applies Lua processing to XML content with surgical editing
func ProcessXML(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
processXMLLogger := xmlLogger.WithPrefix("ProcessXML").WithField("commandName", command.Name).WithField("file", filename)
processXMLLogger.Debug("Starting XML processing for file")
// Parse XML with position tracking
originalElem, err := parseXMLWithPositions(content)
if err != nil {
processXMLLogger.Error("Failed to parse XML: %v", err)
return nil, fmt.Errorf("failed to parse XML: %v", err)
}
processXMLLogger.Debug("Successfully parsed XML content")
// Create Lua state
L, err := NewLuaState()
if err != nil {
processXMLLogger.Error("Error creating Lua state: %v", err)
return nil, fmt.Errorf("error creating Lua state: %v", err)
}
defer L.Close()
// Set filename global
L.SetGlobal("file", lua.LString(filename))
// Create modifiable copy
modifiedElem := deepCopyXMLElement(originalElem)
// Convert to Lua table and set as global
luaTable := xmlElementToLuaTable(L, modifiedElem)
L.SetGlobal("root", luaTable)
processXMLLogger.Debug("Set XML data as Lua global 'root'")
// Build and execute Lua script
luaExpr := BuildJSONLuaScript(command.Lua) // Reuse JSON script builder
processXMLLogger.Debug("Built Lua script from expression: %q", command.Lua)
if err := L.DoString(luaExpr); err != nil {
processXMLLogger.Error("Lua script execution failed: %v\nScript: %s", err, luaExpr)
return nil, fmt.Errorf("lua script execution failed: %v", err)
}
processXMLLogger.Debug("Lua script executed successfully")
// Check if modification flag is set
modifiedVal := L.GetGlobal("modified")
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
processXMLLogger.Debug("Skipping - no modifications indicated by Lua script")
return nil, nil
}
// Get the modified data back from Lua
modifiedTable := L.GetGlobal("root")
if modifiedTable.Type() != lua.LTTable {
processXMLLogger.Error("Expected 'root' to be a table after Lua processing")
return nil, fmt.Errorf("expected 'root' to be a table after Lua processing")
}
// Apply Lua modifications back to XMLElement
luaTableToXMLElement(L, modifiedTable.(*lua.LTable), modifiedElem)
// Find changes between original and modified
changes := findXMLChanges(originalElem, modifiedElem, "")
processXMLLogger.Debug("Found %d changes", len(changes))
if len(changes) == 0 {
return nil, nil
}
// Generate surgical replace commands
commands := applyXMLChanges(changes)
processXMLLogger.Debug("Generated %d replace commands", len(commands))
return commands, nil
}
// xmlElementToLuaTable converts an XMLElement to a Lua table
func xmlElementToLuaTable(L *lua.LState, elem *XMLElement) *lua.LTable {
table := L.CreateTable(0, 4)
table.RawSetString("_tag", lua.LString(elem.Tag))
if len(elem.Attributes) > 0 {
attrs := L.CreateTable(0, len(elem.Attributes))
for name, attr := range elem.Attributes {
attrs.RawSetString(name, lua.LString(attr.Value))
}
table.RawSetString("_attr", attrs)
}
if elem.Text != "" {
table.RawSetString("_text", lua.LString(elem.Text))
}
if len(elem.Children) > 0 {
children := L.CreateTable(len(elem.Children), 0)
for i, child := range elem.Children {
children.RawSetInt(i+1, xmlElementToLuaTable(L, child))
}
table.RawSetString("_children", children)
}
return table
}
// luaTableToXMLElement applies Lua table modifications back to XMLElement
func luaTableToXMLElement(L *lua.LState, table *lua.LTable, elem *XMLElement) {
// Update text
if textVal := table.RawGetString("_text"); textVal.Type() == lua.LTString {
elem.Text = string(textVal.(lua.LString))
}
// Update attributes
if attrVal := table.RawGetString("_attr"); attrVal.Type() == lua.LTTable {
attrTable := attrVal.(*lua.LTable)
// Clear and rebuild attributes
elem.Attributes = make(map[string]XMLAttribute)
attrTable.ForEach(func(key lua.LValue, value lua.LValue) {
if key.Type() == lua.LTString && value.Type() == lua.LTString {
attrName := string(key.(lua.LString))
attrValue := string(value.(lua.LString))
elem.Attributes[attrName] = XMLAttribute{Value: attrValue}
}
})
}
// Update children
if childrenVal := table.RawGetString("_children"); childrenVal.Type() == lua.LTTable {
childrenTable := childrenVal.(*lua.LTable)
newChildren := []*XMLElement{}
// Iterate over array indices
for i := 1; ; i++ {
childVal := childrenTable.RawGetInt(i)
if childVal.Type() == lua.LTNil {
break
}
if childVal.Type() == lua.LTTable {
if i-1 < len(elem.Children) {
// Update existing child
luaTableToXMLElement(L, childVal.(*lua.LTable), elem.Children[i-1])
newChildren = append(newChildren, elem.Children[i-1])
}
}
}
elem.Children = newChildren
}
}

165
processor/xml_real_test.go Normal file
View File

@@ -0,0 +1,165 @@
package processor
import (
"os"
"strings"
"testing"
"cook/utils"
)
func TestRealAfflictionsXML(t *testing.T) {
// Read the real Afflictions.xml file
content, err := os.ReadFile("../testfiles/Afflictions.xml")
if err != nil {
t.Fatalf("Failed to read Afflictions.xml: %v", err)
}
original := string(content)
// Test 1: Double all maxstrength values using helper functions
command := utils.ModifyCommand{
Name: "double_maxstrength",
Lua: `
-- Double all maxstrength attributes in Affliction elements
local afflictions = findElements(root, "Affliction")
for _, affliction in ipairs(afflictions) do
modifyNumAttr(affliction, "maxstrength", function(val) return val * 2 end)
end
modified = true
`,
}
commands, err := ProcessXML(original, command, "Afflictions.xml")
if err != nil {
t.Fatalf("ProcessXML failed: %v", err)
}
if len(commands) == 0 {
t.Fatal("Expected modifications but got none")
}
t.Logf("Generated %d surgical modifications", len(commands))
// Apply modifications
result, count := utils.ExecuteModifications(commands, original)
t.Logf("Applied %d modifications", count)
// Verify specific changes
if !strings.Contains(result, `maxstrength="20"`) {
t.Errorf("Expected to find maxstrength=\"20\" (doubled from 10)")
}
if !strings.Contains(result, `maxstrength="480"`) {
t.Errorf("Expected to find maxstrength=\"480\" (doubled from 240)")
}
if !strings.Contains(result, `maxstrength="12"`) {
t.Errorf("Expected to find maxstrength=\"12\" (doubled from 6)")
}
// Verify formatting preserved (XML declaration should be there)
if !strings.Contains(result, `<?xml`) {
t.Errorf("XML declaration not preserved")
}
// Count lines to ensure structure preserved
origLines := len(strings.Split(original, "\n"))
resultLines := len(strings.Split(result, "\n"))
if origLines != resultLines {
t.Errorf("Line count changed: original %d, result %d", origLines, resultLines)
}
}
func TestRealAfflictionsAttributes(t *testing.T) {
// Read the real file
content, err := os.ReadFile("../testfiles/Afflictions.xml")
if err != nil {
t.Fatalf("Failed to read Afflictions.xml: %v", err)
}
original := string(content)
// Test 2: Modify resistance values using helper functions
command := utils.ModifyCommand{
Name: "increase_resistance",
Lua: `
-- Increase all minresistance and maxresistance by 50%
local effects = findElements(root, "Effect")
for _, effect in ipairs(effects) do
modifyNumAttr(effect, "minresistance", function(val) return val * 1.5 end)
modifyNumAttr(effect, "maxresistance", function(val) return val * 1.5 end)
end
modified = true
`,
}
commands, err := ProcessXML(original, command, "Afflictions.xml")
if err != nil {
t.Fatalf("ProcessXML failed: %v", err)
}
if len(commands) == 0 {
t.Fatal("Expected modifications but got none")
}
t.Logf("Generated %d surgical modifications", len(commands))
// Apply modifications
_, count := utils.ExecuteModifications(commands, original)
t.Logf("Applied %d modifications", count)
// Verify we made resistance modifications
if count < 10 {
t.Errorf("Expected at least 10 resistance modifications, got %d", count)
}
}
func TestRealAfflictionsNestedModifications(t *testing.T) {
// Read the real file
content, err := os.ReadFile("../testfiles/Afflictions.xml")
if err != nil {
t.Fatalf("Failed to read Afflictions.xml: %v", err)
}
original := string(content)
// Test 3: Modify nested Effect attributes using helper functions
command := utils.ModifyCommand{
Name: "modify_effects",
Lua: `
-- Double all amount values in ReduceAffliction elements
local reduces = findElements(root, "ReduceAffliction")
for _, reduce in ipairs(reduces) do
modifyNumAttr(reduce, "amount", function(val) return val * 2 end)
end
modified = true
`,
}
commands, err := ProcessXML(original, command, "Afflictions.xml")
if err != nil {
t.Fatalf("ProcessXML failed: %v", err)
}
if len(commands) == 0 {
t.Fatal("Expected modifications but got none")
}
t.Logf("Generated %d surgical modifications for nested elements", len(commands))
// Apply modifications
result, count := utils.ExecuteModifications(commands, original)
t.Logf("Applied %d modifications", count)
// Verify nested changes (0.001 * 2 = 0.002)
if !strings.Contains(result, `amount="0.002"`) {
t.Errorf("Expected to find amount=\"0.002\" (0.001 * 2)")
}
// Verify we modified the nested elements
if count < 8 {
t.Errorf("Expected at least 8 amount modifications, got %d", count)
}
}