From 74394cbde970cc816fec140902ebf1942bc3b6cb Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Fri, 19 Dec 2025 11:44:07 +0100 Subject: [PATCH] Integrate the xml processing with the rest of the project --- processor/processor.go | 58 +++++++++-- processor/xml.go | 147 ++++++++++++++++++++++++++ processor/xml_integration_test.go | 32 +++--- processor/xml_real_test.go | 165 ++++++++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 22 deletions(-) create mode 100644 processor/xml_real_test.go diff --git a/processor/processor.go b/processor/processor.go index 3064fa0..365b057 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -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` } diff --git a/processor/xml.go b/processor/xml.go index aba001d..a3c3fed 100644 --- a/processor/xml.go +++ b/processor/xml.go @@ -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 + } +} diff --git a/processor/xml_integration_test.go b/processor/xml_integration_test.go index 41634a7..17c84ec 100644 --- a/processor/xml_integration_test.go +++ b/processor/xml_integration_test.go @@ -33,21 +33,21 @@ func TestRealWorldGameXML(t *testing.T) { // Modify: Double all MaxStack values and change Wood weight modElem := deepCopyXMLElement(origElem) - + // Fiber MaxStack: 1000 → 2000 fiberItem := modElem.Children[0] fiberMaxStack := fiberItem.Children[2] valueAttr := fiberMaxStack.Attributes["value"] valueAttr.Value = "2000" fiberMaxStack.Attributes["value"] = valueAttr - + // Wood MaxStack: 500 → 1000 woodItem := modElem.Children[1] woodMaxStack := woodItem.Children[2] valueAttr2 := woodMaxStack.Attributes["value"] valueAttr2.Value = "1000" woodMaxStack.Attributes["value"] = valueAttr2 - + // Wood Weight: 0.05 → 0.10 woodWeight := woodItem.Children[1] weightAttr := woodWeight.Attributes["value"] @@ -56,7 +56,7 @@ func TestRealWorldGameXML(t *testing.T) { // Generate changes changes := findXMLChanges(origElem, modElem, "") - + if len(changes) != 3 { t.Fatalf("Expected 3 changes, got %d", len(changes)) } @@ -102,13 +102,13 @@ func TestAddRemoveMultipleChildren(t *testing.T) { // Remove middle two items, add a new one modElem := deepCopyXMLElement(origElem) - + // Remove shield and potion (indices 1 and 2) modElem.Children = []*XMLElement{ modElem.Children[0], // sword modElem.Children[3], // scroll } - + // Add a new item newItem := &XMLElement{ Tag: "item", @@ -121,14 +121,14 @@ func TestAddRemoveMultipleChildren(t *testing.T) { // Generate changes changes := findXMLChanges(origElem, modElem, "") - + // The algorithm compares by matching indices: // orig[0]=sword vs mod[0]=sword (no change) // orig[1]=shield vs mod[1]=scroll (treated as replace - shows as attribute changes) // orig[2]=potion vs mod[2]=helmet (treated as replace) // orig[3]=scroll (removed) // This is fine - the actual edits will be correct - + if len(changes) == 0 { t.Fatalf("Expected changes, got none") } @@ -170,14 +170,14 @@ func TestModifyAttributesAndText(t *testing.T) { // Modify both items modElem := deepCopyXMLElement(origElem) - + // First item: change damage and text item1 := modElem.Children[0] dmgAttr := item1.Attributes["damage"] dmgAttr.Value = "20" item1.Attributes["damage"] = dmgAttr item1.Text = "Steel Sword" - + // Second item: change damage and type item2 := modElem.Children[1] dmgAttr2 := item2.Attributes["damage"] @@ -253,7 +253,7 @@ func TestNumericAttributeModification(t *testing.T) { // Double all numeric values modElem := deepCopyXMLElement(origElem) - + // Helper to modify numeric attributes modifyNumericAttr := func(attrName string, multiplier float64) { if attr, exists := modElem.Attributes[attrName]; exists { @@ -263,18 +263,18 @@ func TestNumericAttributeModification(t *testing.T) { } } } - + modifyNumericAttr("health", 2.0) modifyNumericAttr("mana", 2.0) modifyNumericAttr("stamina", 2.0) // Generate and apply changes changes := findXMLChanges(origElem, modElem, "") - + if len(changes) != 3 { t.Fatalf("Expected 3 changes, got %d", len(changes)) } - + commands := applyXMLChanges(changes) result, _ := utils.ExecuteModifications(commands, original) @@ -313,12 +313,12 @@ func TestMinimalGitDiff(t *testing.T) { // Generate changes changes := findXMLChanges(origElem, modElem, "") - + // Should be exactly 1 change if len(changes) != 1 { t.Fatalf("Expected exactly 1 change for minimal diff, got %d", len(changes)) } - + if changes[0].OldValue != "75" || changes[0].NewValue != "90" { t.Errorf("Wrong change detected: %v", changes[0]) } diff --git a/processor/xml_real_test.go b/processor/xml_real_test.go new file mode 100644 index 0000000..1edc1db --- /dev/null +++ b/processor/xml_real_test.go @@ -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, `