Implement parsing xml to and from lua

A lot more complex than json.........
This commit is contained in:
2025-03-26 01:36:49 +01:00
parent e31c0e4e8f
commit e5092edf53
2 changed files with 451 additions and 53 deletions

View File

@@ -59,68 +59,55 @@ func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr strin
} }
log.Printf("%#v", result) log.Printf("%#v", result)
// Apply modification // Apply modification based on the result
// if node.Type == xmlquery.AttributeNode { if updatedValue, ok := result.(string); ok {
// // For attribute nodes, update the attribute value // If the result is a simple string, update the node value directly
// node.Parent.Attr = append([]xmlquery.Attr{}, node.Parent.Attr...) xpath.Set(doc, path, updatedValue)
// for i, attr := range node.Parent.Attr { } else if nodeData, ok := result.(map[string]interface{}); ok {
// if attr.Name.Local == node.Data { // If the result is a map, apply more complex updates
// node.Parent.Attr[i].Value = newValue updateNodeFromMap(node, nodeData)
// break }
// }
// }
// } else if node.Type == xmlquery.TextNode {
// // For text nodes, update the text content
// node.Data = newValue
// } else {
// // For element nodes, replace inner text
// // Simple approach: set the InnerText directly if there are no child elements
// if node.FirstChild == nil || (node.FirstChild != nil && node.FirstChild.Type == xmlquery.TextNode && node.FirstChild.NextSibling == nil) {
// if node.FirstChild != nil {
// node.FirstChild.Data = newValue
// } else {
// // Create a new text node and add it as the first child
// textNode := &xmlquery.Node{
// Type: xmlquery.TextNode,
// Data: newValue,
// }
// node.FirstChild = textNode
// }
// } else {
// // Complex case: node has mixed content or child elements
// // Replace just the text content while preserving child elements
// // This is a simplified approach - more complex XML may need more robust handling
// for child := node.FirstChild; child != nil; child = child.NextSibling {
// if child.Type == xmlquery.TextNode {
// child.Data = newValue
// break // Update only the first text node
// }
// }
// }
// }
modCount++ modCount++
} }
// Serialize the modified XML document to string // Serialize the modified XML document to string
// if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode { if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode {
// // If we have an XML declaration, start with it // If we have an XML declaration, start with it
// declaration := doc.FirstChild.OutputXML(true) declaration := doc.FirstChild.OutputXML(true)
// // Remove the firstChild (declaration) before serializing the rest of the document // Remove the firstChild (declaration) before serializing the rest of the document
// doc.FirstChild = doc.FirstChild.NextSibling doc.FirstChild = doc.FirstChild.NextSibling
// return declaration + doc.OutputXML(true), modCount, matchCount, nil return declaration + doc.OutputXML(true), modCount, matchCount, nil
// } }
// return doc.OutputXML(true), modCount, matchCount, nil return doc.OutputXML(true), modCount, matchCount, nil
return "", modCount, matchCount, nil
} }
// ToLua converts XML node values to Lua variables // ToLua converts XML node values to Lua variables
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error { func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
table, err := ToLua(L, data) // Check if data is an xmlquery.Node
if err != nil { node, ok := data.(*xmlquery.Node)
return err 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) L.SetGlobal("v", table)
return nil return nil
} }
@@ -128,5 +115,149 @@ func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
// FromLua gets modified values from Lua // FromLua gets modified values from Lua
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) { func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
luaValue := L.GetGlobal("v") 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"
}
} }

View File

@@ -5,6 +5,9 @@ import (
"testing" "testing"
"regexp" "regexp"
"github.com/antchfx/xmlquery"
lua "github.com/yuin/gopher-lua"
) )
// Helper function to normalize whitespace for comparison // Helper function to normalize whitespace for comparison
@@ -56,7 +59,7 @@ func TestXMLProcessor_Process_NodeValues(t *testing.T) {
</catalog>` </catalog>`
p := &XMLProcessor{} 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 { if err != nil {
t.Fatalf("Error processing content: %v", err) 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 // Add more test cases for specific XML manipulation scenarios below
// These tests would cover additional functionality as the implementation progresses // These tests would cover additional functionality as the implementation progresses
func TestXMLToLua(t *testing.T) {
// Sample XML to test with
xmlStr := `
<root id="1">
<person name="John" age="30">
<address type="home">
<street>123 Main St</street>
<city>Anytown</city>
<zip>12345</zip>
</address>
<contact type="email">john@example.com</contact>
</person>
<person name="Jane" age="28">
<address type="work">
<street>456 Business Ave</street>
<city>Worktown</city>
<zip>54321</zip>
</address>
<contact type="phone">555-1234</contact>
</person>
</root>
`
// 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 := `<test>Original Text</test>`
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 := `<test id="old">Text</test>`
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")
}
})
}