Implement parsing xml to and from lua
A lot more complex than json.........
This commit is contained in:
235
processor/xml.go
235
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"
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
</catalog>`
|
||||
|
||||
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 := `
|
||||
<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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user