Files
BigChef/processor/xml.go

455 lines
13 KiB
Go

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("<!--")
sb.WriteString(node.Data)
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("</")
sb.WriteString(node.Data)
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
}