package processor import ( "fmt" "os" "path/filepath" "strconv" "strings" "github.com/antchfx/xmlquery" lua "github.com/yuin/gopher-lua" ) // XMLProcessor implements the Processor interface using XPath type XMLProcessor struct { Logger Logger } // NewXMLProcessor creates a new XMLProcessor func NewXMLProcessor(logger Logger) *XMLProcessor { return &XMLProcessor{ Logger: logger, } } // Process implements the Processor interface for XMLProcessor func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) { // Use pattern as XPath expression xpathExpr := pattern // Read file content fullPath := filepath.Join(".", filename) content, err := os.ReadFile(fullPath) if err != nil { return 0, 0, fmt.Errorf("error reading file: %v", err) } fileContent := string(content) if p.Logger != nil { p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content)) } // Process the content modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, xpathExpr, luaExpr, originalExpr) if err != nil { return 0, 0, err } // If we made modifications, save the file if modCount > 0 { err = os.WriteFile(fullPath, []byte(modifiedContent), 0644) if err != nil { return 0, 0, fmt.Errorf("error writing file: %v", err) } if p.Logger != nil { p.Logger.Printf("Made %d XML node modifications to %s and saved (%d bytes)", modCount, fullPath, len(modifiedContent)) } } return modCount, matchCount, nil } // ToLua implements the Processor interface for XMLProcessor func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error { // Currently not used directly as this is handled in Process return nil } // FromLua implements the Processor interface for XMLProcessor func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) { // Currently not used directly as this is handled in Process return nil, nil } // XMLNodeToString converts an XML node to a string representation func (p *XMLProcessor) XMLNodeToString(node *xmlquery.Node) string { // Use a simple string representation for now var sb strings.Builder // Start tag with attributes if node.Type == xmlquery.ElementNode { sb.WriteString("<") sb.WriteString(node.Data) // Add attributes for _, attr := range node.Attr { sb.WriteString(" ") sb.WriteString(attr.Name.Local) sb.WriteString("=\"") sb.WriteString(attr.Value) sb.WriteString("\"") } // If self-closing if node.FirstChild == nil { sb.WriteString("/>") return sb.String() } sb.WriteString(">") } else if node.Type == xmlquery.TextNode { // Just write the text content sb.WriteString(node.Data) return sb.String() } else if node.Type == xmlquery.CommentNode { // Write comment sb.WriteString("") return sb.String() } // Add children for child := node.FirstChild; child != nil; child = child.NextSibling { sb.WriteString(p.XMLNodeToString(child)) } // End tag for elements if node.Type == xmlquery.ElementNode { sb.WriteString("") } return sb.String() } // NodeToLuaTable creates a Lua table from an XML node func (p *XMLProcessor) NodeToLuaTable(L *lua.LState, node *xmlquery.Node) lua.LValue { nodeTable := L.NewTable() // Add node name L.SetField(nodeTable, "name", lua.LString(node.Data)) // Add node type switch node.Type { case xmlquery.ElementNode: L.SetField(nodeTable, "type", lua.LString("element")) case xmlquery.TextNode: L.SetField(nodeTable, "type", lua.LString("text")) case xmlquery.AttributeNode: L.SetField(nodeTable, "type", lua.LString("attribute")) case xmlquery.CommentNode: L.SetField(nodeTable, "type", lua.LString("comment")) default: L.SetField(nodeTable, "type", lua.LString("other")) } // Add node text content if it's a text node if node.Type == xmlquery.TextNode { L.SetField(nodeTable, "content", lua.LString(node.Data)) } // Add attributes if it's an element node if node.Type == xmlquery.ElementNode && len(node.Attr) > 0 { attrsTable := L.NewTable() for _, attr := range node.Attr { L.SetField(attrsTable, attr.Name.Local, lua.LString(attr.Value)) } L.SetField(nodeTable, "attributes", attrsTable) } // Add children if any if node.FirstChild != nil { childrenTable := L.NewTable() i := 1 for child := node.FirstChild; child != nil; child = child.NextSibling { // Skip empty text nodes (whitespace) if child.Type == xmlquery.TextNode && strings.TrimSpace(child.Data) == "" { continue } childTable := p.NodeToLuaTable(L, child) childrenTable.RawSetInt(i, childTable) i++ } L.SetField(nodeTable, "children", childrenTable) } return nodeTable } // GetModifiedNode retrieves a modified node from Lua func (p *XMLProcessor) GetModifiedNode(L *lua.LState, originalNode *xmlquery.Node) (*xmlquery.Node, bool) { // Check if we have a node global with changes nodeTable := L.GetGlobal("node") if nodeTable == lua.LNil || nodeTable.Type() != lua.LTTable { return originalNode, false } // Clone the node since we don't want to modify the original clonedNode := *originalNode // For text nodes, check if content was changed if originalNode.Type == xmlquery.TextNode { contentField := L.GetField(nodeTable.(*lua.LTable), "content") if contentField != lua.LNil { if strContent, ok := contentField.(lua.LString); ok { if string(strContent) != originalNode.Data { clonedNode.Data = string(strContent) return &clonedNode, true } } } return originalNode, false } // For element nodes, attributes might have been changed if originalNode.Type == xmlquery.ElementNode { attrsField := L.GetField(nodeTable.(*lua.LTable), "attributes") if attrsField != lua.LNil && attrsField.Type() == lua.LTTable { attrsTable := attrsField.(*lua.LTable) // Check if any attributes changed changed := false for _, attr := range originalNode.Attr { newValue := L.GetField(attrsTable, attr.Name.Local) if newValue != lua.LNil { if strValue, ok := newValue.(lua.LString); ok { if string(strValue) != attr.Value { // Create a new attribute with the changed value for i, a := range clonedNode.Attr { if a.Name.Local == attr.Name.Local { clonedNode.Attr[i].Value = string(strValue) changed = true } } } } } } if changed { return &clonedNode, true } } } // No changes detected return originalNode, false } // SetupXMLHelpers adds XML-specific helper functions to Lua func (p *XMLProcessor) SetupXMLHelpers(L *lua.LState) { // Helper function to create a new XML node L.SetGlobal("new_node", L.NewFunction(func(L *lua.LState) int { nodeName := L.CheckString(1) nodeTable := L.NewTable() L.SetField(nodeTable, "name", lua.LString(nodeName)) L.SetField(nodeTable, "type", lua.LString("element")) L.SetField(nodeTable, "attributes", L.NewTable()) L.SetField(nodeTable, "children", L.NewTable()) L.Push(nodeTable) return 1 })) // Helper function to set an attribute L.SetGlobal("set_attr", L.NewFunction(func(L *lua.LState) int { nodeTable := L.CheckTable(1) attrName := L.CheckString(2) attrValue := L.CheckString(3) attrsTable := L.GetField(nodeTable, "attributes") if attrsTable == lua.LNil { attrsTable = L.NewTable() L.SetField(nodeTable, "attributes", attrsTable) } L.SetField(attrsTable.(*lua.LTable), attrName, lua.LString(attrValue)) return 0 })) // Helper function to add a child node L.SetGlobal("add_child", L.NewFunction(func(L *lua.LState) int { parentTable := L.CheckTable(1) childTable := L.CheckTable(2) childrenTable := L.GetField(parentTable, "children") if childrenTable == lua.LNil { childrenTable = L.NewTable() L.SetField(parentTable, "children", childrenTable) } childrenTbl := childrenTable.(*lua.LTable) childrenTbl.RawSetInt(childrenTbl.Len()+1, childTable) return 0 })) } // ProcessContent implements the Processor interface for XMLProcessor // It processes XML content directly without file I/O func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) { // Parse the XML document doc, err := xmlquery.Parse(strings.NewReader(content)) if err != nil { return "", 0, 0, fmt.Errorf("error parsing XML: %v", err) } // Find nodes matching XPath expression nodes, err := xmlquery.QueryAll(doc, pattern) if err != nil { return "", 0, 0, fmt.Errorf("invalid XPath expression: %v", err) } // Log what we found if p.Logger != nil { p.Logger.Printf("XML mode selected with XPath expression: %s (found %d matching nodes)", pattern, len(nodes)) } if len(nodes) == 0 { if p.Logger != nil { p.Logger.Printf("No XML nodes matched XPath expression: %s", pattern) } return content, 0, 0, nil } // Initialize Lua state L := lua.NewState() defer L.Close() // Setup Lua helper functions if err := InitLuaHelpers(L); err != nil { return "", 0, 0, err } // Register XML-specific helper functions p.SetupXMLHelpers(L) // Track modifications matchCount := len(nodes) modificationCount := 0 modifiedContent := content modifications := []ModificationRecord{} // Process each matching node for i, node := range nodes { // Get the original text representation of this node originalNodeText := p.XMLNodeToString(node) if p.Logger != nil { p.Logger.Printf("Found node #%d: %s", i+1, LimitString(originalNodeText, 100)) } // For text nodes, we'll handle them directly if node.Type == xmlquery.TextNode && node.Parent != nil { // If this is a text node, we'll use its value directly // Get the node's text content textContent := node.Data // Set up Lua environment L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric L.SetGlobal("s1", lua.LString(textContent)) // Try to convert to number if possible if floatVal, err := strconv.ParseFloat(textContent, 64); err == nil { L.SetGlobal("v1", lua.LNumber(floatVal)) } // Execute user's Lua script if err := L.DoString(luaExpr); err != nil { if p.Logger != nil { p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err) } continue // Skip this node on error } // Check for modifications modVal := L.GetGlobal("v1") if v, ok := modVal.(lua.LNumber); ok { // If we have a numeric result, convert it to string newValue := strconv.FormatFloat(float64(v), 'f', -1, 64) if newValue != textContent { // Replace the node content in the document parentStr := p.XMLNodeToString(node.Parent) newParentStr := strings.Replace(parentStr, textContent, newValue, 1) modifiedContent = strings.Replace(modifiedContent, parentStr, newParentStr, 1) modificationCount++ // Record the modification modifications = append(modifications, ModificationRecord{ File: "", OldValue: textContent, NewValue: newValue, Operation: originalExpr, Context: fmt.Sprintf("(XPath: %s)", pattern), }) if p.Logger != nil { p.Logger.Printf("Modified text node #%d: '%s' -> '%s'", i+1, LimitString(textContent, 30), LimitString(newValue, 30)) } } } continue // Move to next node } // Convert the node to a Lua table nodeTable := p.NodeToLuaTable(L, node) // Set the node in Lua global variable for user script L.SetGlobal("node", nodeTable) // Execute user's Lua script if err := L.DoString(luaExpr); err != nil { if p.Logger != nil { p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err) } continue // Skip this node on error } // Get modified node from Lua modifiedNode, changed := p.GetModifiedNode(L, node) if !changed { if p.Logger != nil { p.Logger.Printf("Node #%d was not modified by script", i+1) } continue } // Render the modified node back to XML modifiedNodeText := p.XMLNodeToString(modifiedNode) // Replace just this node in the document if originalNodeText != modifiedNodeText { modifiedContent = strings.Replace( modifiedContent, originalNodeText, modifiedNodeText, 1) modificationCount++ // Record the modification for reporting modifications = append(modifications, ModificationRecord{ File: "", OldValue: LimitString(originalNodeText, 30), NewValue: LimitString(modifiedNodeText, 30), Operation: originalExpr, Context: fmt.Sprintf("(XPath: %s)", pattern), }) if p.Logger != nil { p.Logger.Printf("Modified node #%d", i+1) } } } if p.Logger != nil && modificationCount > 0 { p.Logger.Printf("Made %d XML node modifications", modificationCount) } return modifiedContent, modificationCount, matchCount, nil }