From e5092edf538bd5edb104a6fd0807ccc24a9732e0 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Wed, 26 Mar 2025 01:36:49 +0100 Subject: [PATCH] Implement parsing xml to and from lua A lot more complex than json......... --- processor/xml.go | 235 ++++++++++++++++++++++++++++-------- processor/xml_test.go | 269 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 451 insertions(+), 53 deletions(-) diff --git a/processor/xml.go b/processor/xml.go index 9d02435..920d4ef 100644 --- a/processor/xml.go +++ b/processor/xml.go @@ -59,68 +59,55 @@ func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr strin } log.Printf("%#v", result) - // Apply modification - // if node.Type == xmlquery.AttributeNode { - // // For attribute nodes, update the attribute value - // node.Parent.Attr = append([]xmlquery.Attr{}, node.Parent.Attr...) - // for i, attr := range node.Parent.Attr { - // if attr.Name.Local == node.Data { - // node.Parent.Attr[i].Value = newValue - // break - // } - // } - // } else if node.Type == xmlquery.TextNode { - // // For text nodes, update the text content - // node.Data = newValue - // } else { - // // For element nodes, replace inner text - // // Simple approach: set the InnerText directly if there are no child elements - // if node.FirstChild == nil || (node.FirstChild != nil && node.FirstChild.Type == xmlquery.TextNode && node.FirstChild.NextSibling == nil) { - // if node.FirstChild != nil { - // node.FirstChild.Data = newValue - // } else { - // // Create a new text node and add it as the first child - // textNode := &xmlquery.Node{ - // Type: xmlquery.TextNode, - // Data: newValue, - // } - // node.FirstChild = textNode - // } - // } else { - // // Complex case: node has mixed content or child elements - // // Replace just the text content while preserving child elements - // // This is a simplified approach - more complex XML may need more robust handling - // for child := node.FirstChild; child != nil; child = child.NextSibling { - // if child.Type == xmlquery.TextNode { - // child.Data = newValue - // break // Update only the first text node - // } - // } - // } - // } + // Apply modification based on the result + if updatedValue, ok := result.(string); ok { + // If the result is a simple string, update the node value directly + xpath.Set(doc, path, updatedValue) + } else if nodeData, ok := result.(map[string]interface{}); ok { + // If the result is a map, apply more complex updates + updateNodeFromMap(node, nodeData) + } modCount++ } // Serialize the modified XML document to string - // if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode { - // // If we have an XML declaration, start with it - // declaration := doc.FirstChild.OutputXML(true) - // // Remove the firstChild (declaration) before serializing the rest of the document - // doc.FirstChild = doc.FirstChild.NextSibling - // return declaration + doc.OutputXML(true), modCount, matchCount, nil - // } + 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 - return "", 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 { - table, err := ToLua(L, data) - if err != nil { - return err + // Check if data is an xmlquery.Node + node, ok := data.(*xmlquery.Node) + if !ok { + return fmt.Errorf("expected xmlquery.Node, got %T", data) } + + // Create a simple table with essential data + table := L.NewTable() + + // For element nodes, just provide basic info + L.SetField(table, "type", lua.LString(nodeTypeToString(node.Type))) + L.SetField(table, "name", lua.LString(node.Data)) + L.SetField(table, "value", lua.LString(node.InnerText())) + + // Add attributes if any + if len(node.Attr) > 0 { + attrs := L.NewTable() + for _, attr := range node.Attr { + L.SetField(attrs, attr.Name.Local, lua.LString(attr.Value)) + } + L.SetField(table, "attributes", attrs) + } + L.SetGlobal("v", table) return nil } @@ -128,5 +115,149 @@ func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error { // FromLua gets modified values from Lua func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) { luaValue := L.GetGlobal("v") - return FromLua(L, luaValue) + + // Handle string values directly + if luaValue.Type() == lua.LTString { + return luaValue.String(), nil + } + + // Handle tables (for attributes and more complex updates) + if luaValue.Type() == lua.LTTable { + return luaTableToMap(L, luaValue.(*lua.LTable)), nil + } + + return luaValue.String(), nil +} + +// Simple helper to convert a Lua table to a Go map +func luaTableToMap(L *lua.LState, table *lua.LTable) map[string]interface{} { + result := make(map[string]interface{}) + + table.ForEach(func(k, v lua.LValue) { + if k.Type() == lua.LTString { + key := k.String() + + if v.Type() == lua.LTTable { + result[key] = luaTableToMap(L, v.(*lua.LTable)) + } else { + result[key] = v.String() + } + } + }) + + return result +} + +// Simple helper to convert node type to string +func nodeTypeToString(nodeType xmlquery.NodeType) string { + switch nodeType { + case xmlquery.ElementNode: + return "element" + case xmlquery.TextNode: + return "text" + case xmlquery.AttributeNode: + return "attribute" + default: + return "other" + } +} + +// Helper function to update an XML node from a map +func updateNodeFromMap(node *xmlquery.Node, data map[string]interface{}) { + // Update node value if present + if value, ok := data["value"]; ok { + if strValue, ok := value.(string); ok { + // For element nodes, replace text content + if node.Type == xmlquery.ElementNode { + // Find the first text child if it exists + var textNode *xmlquery.Node + for child := node.FirstChild; child != nil; child = child.NextSibling { + if child.Type == xmlquery.TextNode { + textNode = child + break + } + } + + if textNode != nil { + // Update existing text node + textNode.Data = strValue + } else { + // Create new text node + newText := &xmlquery.Node{ + Type: xmlquery.TextNode, + Data: strValue, + Parent: node, + } + + // Insert at beginning of children + if node.FirstChild != nil { + newText.NextSibling = node.FirstChild + node.FirstChild.PrevSibling = newText + node.FirstChild = newText + } else { + node.FirstChild = newText + node.LastChild = newText + } + } + } else if node.Type == xmlquery.TextNode { + // Directly update text node + node.Data = strValue + } else if node.Type == xmlquery.AttributeNode { + // Update attribute value + if node.Parent != nil { + for i, attr := range node.Parent.Attr { + if attr.Name.Local == node.Data { + node.Parent.Attr[i].Value = strValue + break + } + } + } + } + } + } + + // Update attributes if present + if attrs, ok := data["attributes"].(map[string]interface{}); ok && node.Type == xmlquery.ElementNode { + for name, value := range attrs { + if strValue, ok := value.(string); ok { + // Look for existing attribute + found := false + for i, attr := range node.Attr { + if attr.Name.Local == name { + node.Attr[i].Value = strValue + found = true + break + } + } + + // Add new attribute if not found + if !found { + node.Attr = append(node.Attr, xmlquery.Attr{ + Name: struct { + Space, Local string + }{Local: name}, + Value: strValue, + }) + } + } + } + } +} + +// Helper function to get a string representation of node type +func nodeTypeName(nodeType xmlquery.NodeType) string { + switch nodeType { + case xmlquery.ElementNode: + return "element" + case xmlquery.TextNode: + return "text" + case xmlquery.AttributeNode: + return "attribute" + case xmlquery.CommentNode: + return "comment" + case xmlquery.DeclarationNode: + return "declaration" + default: + return "unknown" + } } diff --git a/processor/xml_test.go b/processor/xml_test.go index 484a77e..fdc8c49 100644 --- a/processor/xml_test.go +++ b/processor/xml_test.go @@ -5,6 +5,9 @@ import ( "testing" "regexp" + + "github.com/antchfx/xmlquery" + lua "github.com/yuin/gopher-lua" ) // Helper function to normalize whitespace for comparison @@ -56,7 +59,7 @@ func TestXMLProcessor_Process_NodeValues(t *testing.T) { ` p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v = v * 2") + result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v.value = v.value * 2") if err != nil { t.Fatalf("Error processing content: %v", err) @@ -1530,3 +1533,267 @@ func TestXMLProcessor_Process_DeepPathNavigation(t *testing.T) { // Add more test cases for specific XML manipulation scenarios below // These tests would cover additional functionality as the implementation progresses + +func TestXMLToLua(t *testing.T) { + // Sample XML to test with + xmlStr := ` + + +
+ 123 Main St + Anytown + 12345 +
+ john@example.com +
+ +
+ 456 Business Ave + Worktown + 54321 +
+ 555-1234 +
+
+ ` + + // Parse the XML + doc, err := xmlquery.Parse(strings.NewReader(xmlStr)) + if err != nil { + t.Fatalf("Failed to parse XML: %v", err) + } + + // Create a new Lua state + L := lua.NewState() + defer L.Close() + + // Create an XML processor + processor := &XMLProcessor{} + + // Test converting the root element to Lua + t.Run("RootElement", func(t *testing.T) { + // Find the root element + root := doc.SelectElement("root") + if root == nil { + t.Fatal("Failed to find root element") + } + + // Convert to Lua + err := processor.ToLua(L, root) + if err != nil { + t.Fatalf("Failed to convert to Lua: %v", err) + } + + // Verify the result + luaTable := L.GetGlobal("v") + if luaTable.Type() != lua.LTTable { + t.Fatalf("Expected table, got %s", luaTable.Type().String()) + } + + // Check element type + typeVal := L.GetField(luaTable, "type") + if typeVal.String() != "element" { + t.Errorf("Expected type 'element', got '%s'", typeVal.String()) + } + + // Check name + nameVal := L.GetField(luaTable, "name") + if nameVal.String() != "root" { + t.Errorf("Expected name 'root', got '%s'", nameVal.String()) + } + + // Check attributes + attrsTable := L.GetField(luaTable, "attributes") + if attrsTable.Type() != lua.LTTable { + t.Fatalf("Expected attributes table, got %s", attrsTable.Type().String()) + } + + idVal := L.GetField(attrsTable, "id") + if idVal.String() != "1" { + t.Errorf("Expected id '1', got '%s'", idVal.String()) + } + + // Check that we have children + childrenTable := L.GetField(luaTable, "children") + if childrenTable.Type() != lua.LTTable { + t.Fatalf("Expected children table, got %s", childrenTable.Type().String()) + } + }) + + // Test converting a nested element to Lua + t.Run("NestedElement", func(t *testing.T) { + // Find a nested element + street := doc.SelectElement("//street") + if street == nil { + t.Fatal("Failed to find street element") + } + + // Convert to Lua + err := processor.ToLua(L, street) + if err != nil { + t.Fatalf("Failed to convert to Lua: %v", err) + } + + // Verify the result + luaTable := L.GetGlobal("v") + if luaTable.Type() != lua.LTTable { + t.Fatalf("Expected table, got %s", luaTable.Type().String()) + } + + // Check element type + typeVal := L.GetField(luaTable, "type") + if typeVal.String() != "element" { + t.Errorf("Expected type 'element', got '%s'", typeVal.String()) + } + + // Check name + nameVal := L.GetField(luaTable, "name") + if nameVal.String() != "street" { + t.Errorf("Expected name 'street', got '%s'", nameVal.String()) + } + + // Check value + valueVal := L.GetField(luaTable, "value") + if valueVal.String() != "123 Main St" { + t.Errorf("Expected value '123 Main St', got '%s'", valueVal.String()) + } + }) + + // Test FromLua with a simple string update + t.Run("FromLuaString", func(t *testing.T) { + // Set up a Lua state with a string value + L := lua.NewState() + defer L.Close() + L.SetGlobal("v", lua.LString("New Value")) + + // Convert from Lua + result, err := processor.FromLua(L) + if err != nil { + t.Fatalf("Failed to convert from Lua: %v", err) + } + + // Verify the result + strResult, ok := result.(string) + if !ok { + t.Fatalf("Expected string result, got %T", result) + } + + if strResult != "New Value" { + t.Errorf("Expected 'New Value', got '%s'", strResult) + } + }) + + // Test FromLua with a complex table update + t.Run("FromLuaTable", func(t *testing.T) { + // Set up a Lua state with a table value + L := lua.NewState() + defer L.Close() + + table := L.NewTable() + L.SetField(table, "value", lua.LString("Updated Text")) + + attrTable := L.NewTable() + L.SetField(attrTable, "id", lua.LString("new-id")) + L.SetField(attrTable, "class", lua.LString("highlight")) + + L.SetField(table, "attributes", attrTable) + L.SetGlobal("v", table) + + // Convert from Lua + result, err := processor.FromLua(L) + if err != nil { + t.Fatalf("Failed to convert from Lua: %v", err) + } + + // Verify the result + mapResult, ok := result.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", result) + } + + // Check value + if value, ok := mapResult["value"]; !ok || value != "Updated Text" { + t.Errorf("Expected value 'Updated Text', got '%v'", value) + } + + // Check attributes + attrs, ok := mapResult["attributes"].(map[string]interface{}) + if !ok { + t.Fatalf("Expected attributes map, got %T", mapResult["attributes"]) + } + + if id, ok := attrs["id"]; !ok || id != "new-id" { + t.Errorf("Expected id 'new-id', got '%v'", id) + } + + if class, ok := attrs["class"]; !ok || class != "highlight" { + t.Errorf("Expected class 'highlight', got '%v'", class) + } + }) + + // Test updateNodeFromMap with a simple value update + t.Run("UpdateNodeValue", func(t *testing.T) { + // Create a simple element to update + xmlStr := `Original Text` + doc, _ := xmlquery.Parse(strings.NewReader(xmlStr)) + node := doc.SelectElement("test") + + // Create update data + updateData := map[string]interface{}{ + "value": "Updated Text", + } + + // Update the node + updateNodeFromMap(node, updateData) + + // Verify the update + if node.InnerText() != "Updated Text" { + t.Errorf("Expected value 'Updated Text', got '%s'", node.InnerText()) + } + }) + + // Test updateNodeFromMap with attribute updates + t.Run("UpdateNodeAttributes", func(t *testing.T) { + // Create an element with attributes + xmlStr := `Text` + doc, _ := xmlquery.Parse(strings.NewReader(xmlStr)) + node := doc.SelectElement("test") + + // Create update data + updateData := map[string]interface{}{ + "attributes": map[string]interface{}{ + "id": "new", + "class": "added", + }, + } + + // Update the node + updateNodeFromMap(node, updateData) + + // Verify the id attribute was updated + idFound := false + classFound := false + for _, attr := range node.Attr { + if attr.Name.Local == "id" { + idFound = true + if attr.Value != "new" { + t.Errorf("Expected id 'new', got '%s'", attr.Value) + } + } + if attr.Name.Local == "class" { + classFound = true + if attr.Value != "added" { + t.Errorf("Expected class 'added', got '%s'", attr.Value) + } + } + } + + if !idFound { + t.Error("Expected to find 'id' attribute but didn't") + } + + if !classFound { + t.Error("Expected to find 'class' attribute but didn't") + } + }) +}